diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -199,7 +199,7 @@
 ```
 niv - dependency manager for Nix projects
 
-version: 0.2.2
+version: 0.2.3
 
 Usage: niv COMMAND
 
@@ -227,13 +227,17 @@
   niv add my-package -v alpha-0.1 -t http://example.com/archive/<version>.zip
 
 Usage: niv add [-n|--name NAME] PACKAGE ([-a|--attribute KEY=VAL] |
-               [-b|--branch BRANCH] | [-o|--owner OWNER] | [-r|--repo REPO] |
-               [-v|--version VERSION] | [-t|--template URL] | [-T|--type TYPE])
+               [-s|--string-attribute KEY=VAL] | [-b|--branch BRANCH] |
+               [-o|--owner OWNER] | [-r|--repo REPO] | [-v|--version VERSION] |
+               [-t|--template URL] | [-T|--type TYPE])
   Add dependency
 
 Available options:
   -n,--name NAME           Set the package name to <NAME>
-  -a,--attribute KEY=VAL   Set the package spec attribute <KEY> to <VAL>
+  -a,--attribute KEY=VAL   Set the package spec attribute <KEY> to <VAL>, where
+                           <VAL> may be JSON.
+  -s,--string-attribute KEY=VAL
+                           Set the package spec attribute <KEY> to <VAL>.
   -b,--branch BRANCH       Equivalent to --attribute branch=<BRANCH>
   -o,--owner OWNER         Equivalent to --attribute owner=<OWNER>
   -r,--repo REPO           Equivalent to --attribute repo=<REPO>
@@ -256,13 +260,17 @@
   niv update nixpkgs             # update nixpkgs
   niv update my-package -v beta-0.2 # update my-package to version "beta-0.2"
 
-Usage: niv update [PACKAGE] ([-a|--attribute KEY=VAL] | [-b|--branch BRANCH] |
+Usage: niv update [PACKAGE] ([-a|--attribute KEY=VAL] |
+                  [-s|--string-attribute KEY=VAL] | [-b|--branch BRANCH] |
                   [-o|--owner OWNER] | [-r|--repo REPO] | [-v|--version VERSION]
                   | [-t|--template URL] | [-T|--type TYPE])
   Update dependencies
 
 Available options:
-  -a,--attribute KEY=VAL   Set the package spec attribute <KEY> to <VAL>
+  -a,--attribute KEY=VAL   Set the package spec attribute <KEY> to <VAL>, where
+                           <VAL> may be JSON.
+  -s,--string-attribute KEY=VAL
+                           Set the package spec attribute <KEY> to <VAL>.
   -b,--branch BRANCH       Equivalent to --attribute branch=<BRANCH>
   -o,--owner OWNER         Equivalent to --attribute owner=<OWNER>
   -r,--repo REPO           Equivalent to --attribute repo=<REPO>
diff --git a/niv.cabal b/niv.cabal
--- a/niv.cabal
+++ b/niv.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 4ae2ca2b71f11737dafa898e811c9e1a8f33a82bb9c5365bff2ff9e9d89dab93
+-- hash: 340462761c19c599af0d4d6847a470a4487bdb15dabd5a38b9ce07d90a114c8d
 
 name:           niv
-version:        0.2.2
+version:        0.2.3
 synopsis:       Easy dependency management for Nix projects
 description:    Easy dependency management for Nix projects.
 category:       Development
diff --git a/nix/sources.nix b/nix/sources.nix
--- a/nix/sources.nix
+++ b/nix/sources.nix
@@ -1,8 +1,56 @@
 # This file has been generated by Niv.
 
-# A record, from name to path, of the third-party packages
-with rec
-{
+let
+
+  #
+  # The fetchers. fetch_<type> fetches specs of type <type>.
+  #
+
+  fetch_file = spec:
+    if spec.builtin or true then
+      builtins_fetchurl { inherit (spec) url sha256; }
+    else
+      pkgs.fetchurl { inherit (spec) url sha256; };
+
+  fetch_tarball = spec:
+    if spec.builtin or true then
+      builtins_fetchTarball { inherit (spec) url sha256; }
+    else
+      pkgs.fetchzip { inherit (spec) url sha256; };
+
+  fetch_builtin-tarball = spec:
+    builtins.trace
+      ''
+        WARNING:
+          The niv type "builtin-tarball" will soon be deprecated. You should
+          instead use `builtin = true`.
+
+          $ niv modify <package> -a type=tarball -a builtin=true
+      ''
+      builtins_fetchTarball { inherit (spec) url sha256; };
+
+  fetch_builtin-url = spec:
+    builtins.trace
+      ''
+        WARNING:
+          The niv type "builtin-url" will soon be deprecated. You should
+          instead use `builtin = true`.
+
+          $ niv modify <package> -a type=file -a builtin=true
+      ''
+      (builtins_fetchurl { inherit (spec) url sha256; });
+
+  #
+  # The sources to fetch.
+  #
+
+  sources = builtins.fromJSON (builtins.readFile ./sources.json);
+
+  #
+  # Various helpers
+  #
+
+  # The set of packages used when specs are fetched using non-builtins.
   pkgs =
     if hasNixpkgsPath
     then
@@ -21,6 +69,30 @@
         add a package called "nixpkgs" to your sources.json.
       '';
 
+  hasNixpkgsPath = (builtins.tryEval <nixpkgs>).success;
+  hasThisAsNixpkgsPath =
+    (builtins.tryEval <nixpkgs>).success && <nixpkgs> == ./.;
+
+  # The actual fetching function.
+  fetch = name: spec:
+
+    if ! builtins.hasAttr "type" spec then
+      abort "ERROR: niv spec ${name} does not have a 'type' attribute"
+    else if spec.type == "file" then fetch_file spec
+    else if spec.type == "tarball" then fetch_tarball spec
+    else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
+    else if spec.type == "builtin-url" then fetch_builtin-url spec
+    else
+      abort "ERROR: niv spec ${name} has unknown type ${builtins.fromJSON spec.type}";
+
+  # Ports of functions for older nix versions
+
+  # a Nix version of mapAttrs if the built-in doesn't exist
+  mapAttrs = builtins.mapAttrs or (
+    f: set: with builtins;
+    listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
+  );
+
   # fetchTarball version that is compatible between all the versions of Nix
   builtins_fetchTarball = { url, sha256 }@attrs:
     let
@@ -41,56 +113,12 @@
       else
         fetchurl attrs;
 
-  # A wrapper around pkgs.fetchzip that has inspectable arguments,
-  # annoyingly this means we have to specify them
-  fetchzip = { url, sha256 }@attrs: pkgs.fetchzip attrs;
-
-  # A wrapper around pkgs.fetchurl that has inspectable arguments,
-  # annoyingly this means we have to specify them
-  fetchurl = { url, sha256 }@attrs: pkgs.fetchurl attrs;
-
-  hasNixpkgsPath = (builtins.tryEval <nixpkgs>).success;
-  hasThisAsNixpkgsPath =
-    (builtins.tryEval <nixpkgs>).success && <nixpkgs> == ./.;
-
-  sources = builtins.fromJSON (builtins.readFile ./sources.json);
-
-  mapAttrs = builtins.mapAttrs or (
-    f: set: with builtins;
-    listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
-  );
-
-  # borrowed from nixpkgs
-  functionArgs = f: f.__functionArgs or (builtins.functionArgs f);
-  callFunctionWith = autoArgs: f: args:
-    let
-      auto = builtins.intersectAttrs (functionArgs f) autoArgs;
-    in
-      f (auto // args);
-
-  getFetcher = spec:
-    let
-      fetcherName =
-        if builtins.hasAttr "type" spec
-        then builtins.getAttr "type" spec
-        else "builtin-tarball";
-    in
-      builtins.getAttr fetcherName {
-        "tarball" = fetchzip;
-        "builtin-tarball" = builtins_fetchTarball;
-        "file" = fetchurl;
-        "builtin-url" = builtins_fetchurl;
-      };
-};
-# NOTE: spec must _not_ have an "outPath" attribute
+in
 mapAttrs (
-  _: spec:
+  name: spec:
     if builtins.hasAttr "outPath" spec
     then abort
       "The values in sources.json should not have an 'outPath' attribute"
     else
-      if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec
-      then
-        spec // { outPath = callFunctionWith spec (getFetcher spec) {}; }
-      else spec
+      spec // { outPath = fetch name spec; }
 ) sources
diff --git a/src/Niv/Cli.hs b/src/Niv/Cli.hs
--- a/src/Niv/Cli.hs
+++ b/src/Niv/Cli.hs
@@ -11,6 +11,8 @@
 import Control.Applicative
 import Control.Monad
 import Data.Aeson ((.=))
+import Data.Bifunctor
+import Data.Maybe
 import Data.Char (isSpace)
 import Data.Functor
 import Data.HashMap.Strict.Extended
@@ -70,45 +72,57 @@
 
 parsePackageSpec :: Opts.Parser PackageSpec
 parsePackageSpec =
-    (PackageSpec . HMS.fromList . fmap fixupAttributes) <$>
+    (PackageSpec . HMS.fromList) <$>
       many parseAttribute
   where
-    parseAttribute :: Opts.Parser (T.Text, T.Text)
+    parseAttribute :: Opts.Parser (T.Text, Aeson.Value)
     parseAttribute =
-      Opts.option (Opts.maybeReader parseKeyVal)
+      Opts.option (Opts.maybeReader parseKeyValJSON)
         ( Opts.long "attribute" <>
           Opts.short 'a' <>
           Opts.metavar "KEY=VAL" <>
-          Opts.help "Set the package spec attribute <KEY> to <VAL>"
-        ) <|> shortcutAttributes <|>
-      (("url_template",) <$> Opts.strOption
+          Opts.help "Set the package spec attribute <KEY> to <VAL>, where <VAL> may be JSON."
+        ) <|>
+      Opts.option (Opts.maybeReader (parseKeyVal Aeson.toJSON))
+        ( Opts.long "string-attribute" <>
+          Opts.short 's' <>
+          Opts.metavar "KEY=VAL" <>
+          Opts.help "Set the package spec attribute <KEY> to <VAL>."
+        ) <|>
+      shortcutAttributes <|>
+      ((("url_template",) . Aeson.String) <$> Opts.strOption
         ( Opts.long "template" <>
           Opts.short 't' <>
           Opts.metavar "URL" <>
           Opts.help "Used during 'update' when building URL. Occurrences of <foo> are replaced with attribute 'foo'."
         )) <|>
-      (("type",) <$> Opts.strOption
+      ((("type",) . Aeson.String) <$> Opts.strOption
         ( Opts.long "type" <>
           Opts.short 'T' <>
           Opts.metavar "TYPE" <>
           Opts.help "The type of the URL target. The value can be either 'file' or 'tarball'. If not set, the value is inferred from the suffix of the URL."
         ))
 
-    -- Parse "key=val" into ("key", "val")
-    parseKeyVal :: String -> Maybe (T.Text, T.Text)
-    parseKeyVal str = case span (/= '=') str of
-      (key, '=':val) -> Just (T.pack key, T.pack val)
+    parseKeyValJSON = parseKeyVal $ \x ->
+      fromMaybe (Aeson.toJSON x) (Aeson.decodeStrict (B8.pack x))
+
+    -- Parse "key=val" into ("key", val)
+    parseKeyVal
+      :: (String -> Aeson.Value)  -- ^ how to convert to JSON
+      -> String -> Maybe (T.Text, Aeson.Value)
+    parseKeyVal toJSON str = case span (/= '=') str of
+      (key, '=':val) -> Just (T.pack key, toJSON val)
       _ -> Nothing
 
     -- Shortcuts for common attributes
-    shortcutAttributes :: Opts.Parser (T.Text, T.Text)
+    shortcutAttributes :: Opts.Parser (T.Text, Aeson.Value)
     shortcutAttributes = foldr (<|>) empty $ mkShortcutAttribute <$>
       [ "branch", "owner", "repo", "version" ]
 
     -- TODO: infer those shortcuts from 'Update' keys
-    mkShortcutAttribute :: T.Text -> Opts.Parser (T.Text, T.Text)
+    mkShortcutAttribute :: T.Text -> Opts.Parser (T.Text, Aeson.Value)
     mkShortcutAttribute = \case
-      attr@(T.uncons -> Just (c,_)) -> (attr,) <$> Opts.strOption
+      attr@(T.uncons -> Just (c,_)) -> fmap (second Aeson.String) $ (attr,) <$> Opts.strOption
         ( Opts.long (T.unpack attr) <>
           Opts.short c <>
           Opts.metavar (T.unpack $ T.toUpper attr) <>
@@ -119,9 +133,6 @@
             )
         )
       _ -> empty
-
-    fixupAttributes :: (T.Text, T.Text) -> (T.Text, Aeson.Value)
-    fixupAttributes (k, v) = (k, Aeson.String v)
 
 parsePackage :: Opts.Parser (PackageName, PackageSpec)
 parsePackage = (,) <$> parsePackageName <*> parsePackageSpec
diff --git a/src/Niv/Sources.hs b/src/Niv/Sources.hs
--- a/src/Niv/Sources.hs
+++ b/src/Niv/Sources.hs
@@ -134,6 +134,7 @@
   | V8
   | V9
   | V10
+  | V11
   deriving stock (Bounded, Enum, Eq)
 
 -- | A user friendly version
@@ -149,6 +150,7 @@
     V8 -> "8"
     V9 -> "9"
     V10 -> "10"
+    V11 -> "11"
 
 latestVersionMD5 :: T.Text
 latestVersionMD5 = sourcesVersionToMD5 maxBound
@@ -171,6 +173,7 @@
     V8 -> "e8b860753dd7fa1fd7b805dd836eb607"
     V9 -> "87149616c1b3b1e5aa73178f91c20b53"
     V10 -> "d8625c0a03dd935e1c79f46407faa8d3"
+    V11 -> "8a95b7d93b16f7c7515d98f49b0ec741"
 
 -- | The MD5 sum of ./nix/sources.nix
 sourcesNixMD5 :: IO T.Text
