jsonnet Transform

Execute a Jsonnet expression to transform and combine input data. This transform uses go-jsonnet, a native Go implementation of Jsonnet.

Usage

node:
  config:
    from_file: config.json
  env:
    from_file: env.yaml
  result:
    to_file: result.json
    transform:
      jsonnet: |
        {
          name: config.name,
          env: env.environment,
          merged: std.mergePatch(config, env)
        }

Variables

All nodes are available as local variables in the Jsonnet expression.

Single-Document Nodes

For nodes with one document, access directly:

// Node with single file
config

// Access nested fields
config.database.host

// Access array elements
users[0].name

Multi-Document Nodes

For nodes with multiple documents, access by document name:

node:
  inputs:
    from_file:
      - base.json           # Document "base"
      - override.json       # Document "override"
  result:
    transform:
      jsonnet: |
        {
          base: inputs.base,
          override: inputs.override,
          merged: std.mergePatch(inputs.base, inputs.override)
        }

Hybrid Nodes

In hybrid nodes (nodes with both from_* and transform), the input data is available as _self_:

node:
  config:
    from_file: base.json
    to_file: output.json
    transform:
      jsonnet: |
        _self_ + { extra: "added" }

Note: The variable is named _self_ (not self) because self is a reserved keyword in Jsonnet.

Jsonnet Features

Object Merge

Use + for shallow merge or std.mergePatch for deep merge:

transform:
  jsonnet: |
    // Shallow merge (later object wins for conflicting keys)
    base + override
    
    // Deep merge (recursive merge of nested objects)
    std.mergePatch(base, override)

Array Operations

transform:
  jsonnet: |
    {
      count: std.length(data.items),
      sum: std.foldl(function(a, b) a + b, data.items, 0),
      doubled: [x * 2 for x in data.items],
      filtered: [x for x in data.items if x > 10]
    }

Conditionals

transform:
  jsonnet: |
    {
      logLevel: if config.debug then "debug" else "info",
      settings: if config.env == "production"
        then { ssl: true, timeout: 30 }
        else { ssl: false, timeout: 60 }
    }

Functions

transform:
  jsonnet: |
    local double(x) = x * 2;
    local greet(name) = "Hello, " + name;
    
    {
      doubled: double(config.value),
      greeting: greet(config.name)
    }

String Formatting

transform:
  jsonnet: |
    {
      url: "https://%s:%d" % [config.host, config.port],
      message: "Hello, %(name)s!" % config
    }

Standard Library Functions

Jsonnet includes a rich standard library. Common functions:

Function Example Description
std.mergePatch(a, b) Deep merge two objects
std.length(arr) Array/string/object length
std.map(f, arr) Map function over array
std.filter(f, arr) Filter array by predicate
std.foldl(f, arr, init) Fold/reduce array
std.sort(arr) Sort array
std.uniq(arr) Remove duplicates
std.objectFields(obj) Get object keys
std.objectValues(obj) Get object values
std.toString(v) Convert to string
std.parseInt(s) Parse string to int
std.manifestJsonEx(v, indent) Format as JSON string

For a complete reference, see the Jsonnet standard library documentation.

Comparison with Other Transforms

Aspect jsonnet jq gotmpl_inline
Syntax Jsonnet jq Go templates
Merge + / std.mergePatch * Manual
Functions First-class Built-in Sprig
Imports Supported Not supported Not supported
Comments // and /* */ # {{/* */}}
Best for Complex logic, reuse Data transformation Text generation

Use jsonnet when:

  • You need complex logic with functions and local variables
  • You want to reuse code via imports (when using jsonnet files)
  • You prefer a more programming-language-like syntax
  • You're already familiar with Jsonnet

Use jq when:

  • You need powerful array filtering and mapping
  • You want a more concise syntax for simple transformations
  • You're already familiar with jq

Examples

Basic Transformation

node:
  source:
    from_file: config.json
  minimal:
    to_file: minimal.json
    transform:
      jsonnet: |
        {
          appName: source.application.name,
          version: source.application.version,
          port: source.server.port
        }

Environment-Specific Config

node:
  base:
    from_file: base.yaml
  env:
    from_file: production.yaml
  config:
    to_file: config.json
    transform:
      jsonnet: |
        std.mergePatch(base, env) + {
          generated: true,
          timestamp: std.extVar("timestamp")  // If needed
        }

Processing Arrays

node:
  users:
    from_file: users.json
  report:
    to_file: report.json
    transform:
      jsonnet: |
        local adults = [u for u in users if u.age >= 18];
        local names = [u.name for u in users];
        {
          total: std.length(users),
          adults: std.length(adults),
          names: std.sort(names)
        }

Error Handling

Common errors and solutions:

Error Cause Solution
"Expected token IDENTIFIER but got self" Using self as variable Use _self_ for hybrid node data
"Unknown variable" Referencing undefined node Check node name spelling
"Field does not exist" Accessing missing field Use std.get(obj, field, default)