From YAML to Nix

It's smaller than you think. Three punctuation changes and you're there.

The entire syntax difference: three rules

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.

JSONNix
Key-value separator"key": valuekey = value;
Keys"quoted"unquoted
Entry terminator, (comma); (semicolon)

That's it. Same braces, same brackets, same strings. Here's a side-by-side:

JSON
{
  "name": "api-server",
  "port": 8080,
  "tags": ["web", "prod"]
}
YAML
name: api-server
port: 8080
tags:
  - web
  - prod
Nix
{
  name = "api-server";
  port = 8080;
  tags = ["web" "prod"];
}
Nix lists don't use commas. Items are just separated by spaces: ["web" "prod"] not ["web", "prod"]. That's the one thing that might trip you up. Everything else is what you'd expect.

Dot notation: flatten nested keys

Nix lets you collapse nested attribute sets with dots. Same result, less indentation.

Nested (JSON-style)
{
  metadata = {
    name = "api-server";
    labels = {
      app = "api-server";
    };
  };
}
With dot notation
{
  metadata.name = "api-server";
  metadata.labels.app = "api-server";
}
Both produce the exact same value. Use whichever is clearer. Deep nesting? Dots. Lots of sibling keys? Braces.

Nix imports YAML and JSON natively

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
};
Migrate incrementally. Keep your existing YAML/JSON configs. Import them into Nix. Start getting compiler benefits immediately. Rewrite files to native Nix at your own pace — or don't. It works either way.

A real resource: YAML vs Nix

A Kubernetes Deployment. Same fields, same structure, same meaning. The Nix version is just data in a different syntax.

YAML
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
Nix (just data)
{
  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";
        };
      }];
    };
  };
}
No functions. No imports. No framework. This is just a Nix attribute set that describes the same data as the YAML. You could paste this into a Nix REPL right now and it would evaluate to a value.
Notice the dot notation in action. 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.

From data to compiler-checked resource

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.

Plain Nix data
# A value. Nothing tracks it.
{
  apiVersion = "apps/v1";
  kind = "Deployment";
  metadata.name = "api-server";
  ...
}
kix resource
# Now the compiler sees it.
deployment = scope.mkResource {
  apiVersion = "apps/v1";
  kind = "Deployment";
  name = "api-server";
  ...
};
Same data. One wrapper. The fields inside are unchanged. 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.

Continue with the main walkthrough →