Skip to content

Instantly share code, notes, and snippets.

@Scoder12
Last active March 24, 2024 22:19
Show Gist options
  • Save Scoder12/0538252ed4b82d65e59115075369d34d to your computer and use it in GitHub Desktop.
Save Scoder12/0538252ed4b82d65e59115075369d34d to your computer and use it in GitHub Desktop.
Converts JSON objects into nix (hackishly).
"""Converts JSON objects into nix (hackishly)."""
import sys
import json
INDENT = " " * 2
def strip_comments(t):
# fixme: doesn't work if JSON strings contain //
return "\n".join(l.partition("//")[0] for l in t.split("\n"))
def indent(s):
return "\n".join(INDENT + i for i in s.split("\n"))
def nix_stringify(s):
# fixme: this doesn't handle string interpolation and possibly has more bugs
return json.dumps(s)
def sanitize_key(s):
if s and s.isalnum() and not s[0].isdigit():
return s
return nix_stringify(s)
def flatten_obj_item(k, v):
keys = [k]
val = v
while isinstance(val, dict) and len(val) == 1:
k = next(iter(val.keys()))
keys.append(k)
val = val[k]
return keys, val
def fmt_object(obj, flatten):
fields = []
for k, v in obj.items():
if flatten:
keys, val = flatten_obj_item(k, v)
formatted_key = ".".join(sanitize_key(i) for i in keys)
else:
formatted_key = sanitize_key(k)
val = v
fields.append(f"{formatted_key} = {fmt_any(val, flatten)};")
return "{\n" + indent("\n".join(fields)) + "\n}"
def fmt_array(o, flatten):
body = indent("\n".join(fmt_any(i, flatten) for i in o))
return f"[\n{body}\n]"
def fmt_any(o, flatten):
if isinstance(o, str) or isinstance(o, bool) or isinstance(o, int):
return json.dumps(o)
if isinstance(o, list):
return fmt_array(o, flatten)
if isinstance(o, dict):
return fmt_object(o, flatten)
raise TypeError(f"Unknown type {type(o)!r}")
def main():
flatten = "--flatten" in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith("--")]
if len(args) < 1:
print(f"Usage: {sys.argv[0]} [--flatten] <file.json>", file=sys.stderr)
sys.exit(1)
with open(args[0], "r") as f:
data = json.loads(strip_comments(f.read()))
print(fmt_any(data, flatten=flatten))
if __name__ == "__main__":
main()
@Scoder12
Copy link
Author

Scoder12 commented May 23, 2021

Example:

❯ cat /tmp/test.json
{
  "a": {
    "b": {
      "c": 1
    }
  }
}
❯ python json2nix.py /tmp/test.json
{
  a = {
    b = {
      c = 1;
    };
  };
}
❯ python json2nix.py --flatten /tmp/test.json
{
  a.b.c = 1;
}

@IvanTurgenev
Copy link

thanks man so useful

@Scoder12
Copy link
Author

You're welcome. Thanks for reaching out.

@adamcstephens
Copy link

Here's a package for using in Nix :)

json2nix = pkgs.writeScriptBin "json2nix" ''
  ${pkgs.python3}/bin/python ${pkgs.fetchurl {
    url = "https://gist.githubusercontent.com/Scoder12/0538252ed4b82d65e59115075369d34d/raw/e86d1d64d1373a497118beb1259dab149cea951d/json2nix.py";
    hash = "sha256-ROUIrOrY9Mp1F3m+bVaT+m8ASh2Bgz8VrPyyrQf9UNQ=";
  }} $@
'';

@dzmitry-lahoda
Copy link

thank you. very nice. i hope nix builtins will get this one too :)

@adamcstephens
Copy link

It has some inaccuracies imo, so needs a bit of improvement. It needless quotes some attribute names, and can't handle URLs at all.

@Scoder12
Copy link
Author

Yes, this was a quick hack (as it says at the top) and it wasn't meant to be the definitive implementation or anything. Since it seems like theres demand I could make a better version

@adamcstephens
Copy link

Either way, thanks. :)

@eclairevoyant
Copy link

@dzmitry-lahoda builtins can basically handle this already:

$ nix-instantiate --eval -E 'builtins.fromJSON (builtins.readFile ./test.json)'
{ a = { b = { c = 1; }; }; }

and can be piped through a formatter:

$ nix-instantiate --eval -E 'builtins.fromJSON (builtins.readFile ./test.json)' | nixfmt
{
  a = {
    b = {
      c = 1;
    };
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment