It's smaller than you think. Three punctuation changes and you're there.
If you can read JSON, you can read Nix. The data structure is the same — nested key-value maps and lists. Only the punctuation differs.
| JSON | Nix | |
| Key-value separator | "key": value | key = value; |
| Keys | "quoted" | unquoted |
| Entry terminator | , (comma) | ; (semicolon) |
That's it. Same braces, same brackets, same strings. Here's a side-by-side:
{
"name": "api-server",
"port": 8080,
"tags": ["web", "prod"]
}
name: api-server port: 8080 tags: - web - prod
{
name = "api-server";
port = 8080;
tags = ["web" "prod"];
}
["web" "prod"] not ["web", "prod"].
That's the one thing that might trip you up. Everything else is what you'd expect.
Nix lets you collapse nested attribute sets with dots. Same result, less indentation.
{
metadata = {
name = "api-server";
labels = {
app = "api-server";
};
};
}
{
metadata.name = "api-server";
metadata.labels.app = "api-server";
}
You don't have to rewrite everything on day one. Nix can read your existing files directly.
# Import a JSON file — it becomes a native Nix value config = builtins.fromJSON (builtins.readFile ./config.json); # Import a YAML file — same thing values = builtins.fromYAML (builtins.readFile ./values.yaml); # Use them like any other Nix value deployment = scope.mkResource { kind = "Deployment"; name = config.name; # ← from your JSON spec.replicas = values.replicas; # ← from your YAML };
A Kubernetes Deployment. Same fields, same structure, same meaning. The Nix version is just data in a different syntax.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api-server
image: myregistry/api:1.4.2
ports:
- containerPort: 8080
resources:
requests:
memory: 256Mi
cpu: 250m
{
apiVersion = "apps/v1";
kind = "Deployment";
metadata.name = "api-server";
metadata.labels.app = "api-server";
spec = {
replicas = 3;
selector.matchLabels.app = "api-server";
template = {
metadata.labels.app = "api-server";
spec.containers = [{
name = "api-server";
image = "myregistry/api:1.4.2";
ports = [{ containerPort = 8080; }];
resources.requests = {
memory = "256Mi";
cpu = "250m";
};
}];
};
};
}
metadata.name = "api-server" instead of nesting metadata = { name = ... }.
selector.matchLabels.app instead of three levels of braces.
Wherever it's clearer, flatten with dots.
The Nix value above is just data. To get compiler tracking — dependency graphs,
hash-based diffing, automatic wiring — you wrap it with scope.mkResource.
One line changes.
# A value. Nothing tracks it. { apiVersion = "apps/v1"; kind = "Deployment"; metadata.name = "api-server"; ... }
# Now the compiler sees it. deployment = scope.mkResource { apiVersion = "apps/v1"; kind = "Deployment"; name = "api-server"; ... };
mkResource registers the resource in the dependency graph and
exposes its properties through .out — so other resources can
reference it safely. That's when things get interesting.