diff --git a/src/libutil/tests/json.cc b/src/libutil/tests/json.cc
new file mode 100644
index 0000000000000000000000000000000000000000..dea73f53a91b084fb038ed0c8be750bb8cb032ed
--- /dev/null
+++ b/src/libutil/tests/json.cc
@@ -0,0 +1,193 @@
+#include "json.hh"
+#include <gtest/gtest.h>
+#include <sstream>
+
+namespace nix {
+
+    /* ----------------------------------------------------------------------------
+     * toJSON
+     * --------------------------------------------------------------------------*/
+
+    TEST(toJSON, quotesCharPtr) {
+        const char* input = "test";
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "\"test\"");
+    }
+
+    TEST(toJSON, quotesStdString) {
+        std::string input = "test";
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "\"test\"");
+    }
+
+    TEST(toJSON, convertsNullptrtoNull) {
+        auto input = nullptr;
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "null");
+    }
+
+    TEST(toJSON, convertsNullToNull) {
+        const char* input = 0;
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "null");
+    }
+
+
+    TEST(toJSON, convertsFloat) {
+        auto input = 1.024f;
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "1.024");
+    }
+
+    TEST(toJSON, convertsDouble) {
+        const double input = 1.024;
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "1.024");
+    }
+
+    TEST(toJSON, convertsBool) {
+        auto input = false;
+        std::stringstream out;
+        toJSON(out, input);
+
+        ASSERT_EQ(out.str(), "false");
+    }
+
+    TEST(toJSON, quotesTab) {
+        std::stringstream out;
+        toJSON(out, "\t");
+
+        ASSERT_EQ(out.str(), "\"\\t\"");
+    }
+
+    TEST(toJSON, quotesNewline) {
+        std::stringstream out;
+        toJSON(out, "\n");
+
+        ASSERT_EQ(out.str(), "\"\\n\"");
+    }
+
+    TEST(toJSON, quotesCreturn) {
+        std::stringstream out;
+        toJSON(out, "\r");
+
+        ASSERT_EQ(out.str(), "\"\\r\"");
+    }
+
+    TEST(toJSON, quotesCreturnNewLine) {
+        std::stringstream out;
+        toJSON(out, "\r\n");
+
+        ASSERT_EQ(out.str(), "\"\\r\\n\"");
+    }
+
+    TEST(toJSON, quotesDoublequotes) {
+        std::stringstream out;
+        toJSON(out, "\"");
+
+        ASSERT_EQ(out.str(), "\"\\\"\"");
+    }
+
+    TEST(toJSON, substringEscape) {
+        std::stringstream out;
+        const char *s = "foo\t";
+        toJSON(out, s+3, s + strlen(s));
+
+        ASSERT_EQ(out.str(), "\"\\t\"");
+    }
+
+    /* ----------------------------------------------------------------------------
+     * JSONObject
+     * --------------------------------------------------------------------------*/
+
+    TEST(JSONObject, emptyObject) {
+        std::stringstream out;
+        {
+            JSONObject t(out);
+        }
+        ASSERT_EQ(out.str(), "{}");
+    }
+
+    TEST(JSONObject, objectWithList) {
+        std::stringstream out;
+        {
+            JSONObject t(out);
+            auto l = t.list("list");
+            l.elem("element");
+        }
+        ASSERT_EQ(out.str(), R"#({"list":["element"]})#");
+    }
+
+    TEST(JSONObject, objectWithListIndent) {
+        std::stringstream out;
+        {
+            JSONObject t(out, true);
+            auto l = t.list("list");
+            l.elem("element");
+        }
+        ASSERT_EQ(out.str(),
+R"#({
+  "list": [
+    "element"
+  ]
+})#");
+    }
+
+    TEST(JSONObject, objectWithPlaceholderAndList) {
+        std::stringstream out;
+        {
+            JSONObject t(out);
+            auto l = t.placeholder("list");
+            l.list().elem("element");
+        }
+
+        ASSERT_EQ(out.str(), R"#({"list":["element"]})#");
+    }
+
+    TEST(JSONObject, objectWithPlaceholderAndObject) {
+        std::stringstream out;
+        {
+            JSONObject t(out);
+            auto l = t.placeholder("object");
+            l.object().attr("key", "value");
+        }
+
+        ASSERT_EQ(out.str(), R"#({"object":{"key":"value"}})#");
+    }
+
+    /* ----------------------------------------------------------------------------
+     * JSONList
+     * --------------------------------------------------------------------------*/
+
+    TEST(JSONList, empty) {
+        std::stringstream out;
+        {
+            JSONList l(out);
+        }
+        ASSERT_EQ(out.str(), R"#([])#");
+    }
+
+    TEST(JSONList, withElements) {
+        std::stringstream out;
+        {
+            JSONList l(out);
+            l.elem("one");
+            l.object();
+            l.placeholder().write("three");
+        }
+        ASSERT_EQ(out.str(), R"#(["one",{},"three"])#");
+    }
+}
+