jq Transform

Execute a jq expression to transform and combine input data. This transform uses gojq, a pure Go implementation of jq.

Usage

node:
  config:
    from_file: config.json
  env:
    from_file: env.yaml
  result:
    to_file: result.json
    transform:
      jq: |
        {
          name: $config.name,
          env: $env.environment,
          merged: ($config * $env)
        }

Variables

The jq expression has access to all nodes as variables.

The $self Variable

In hybrid nodes (nodes with both from_* and transform), the input data is available via $self:

node:
  config:
    from_file: base.json
    to_file: output.json
    transform:
      jq: $self * {extra: "added"}

For multi-document inputs, access documents via $self.docName:

node:
  config:
    from_file:
      - base.json
      - override.json
    to_file: output.json
    transform:
      jq: $self.base * $self.override

Note: $self is only available in hybrid nodes. Using it in a transform-only node will result in an error.

Single-Document Nodes

For nodes with one document (single file, single URL, inline data), 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 (file arrays, globs), access specific documents by name:

node:
  inputs:
    from_file:
      - base.json           # Document "base"
      - override.json       # Document "override"
  result:
    transform:
      jq: |
        {
          base: $inputs.base,
          override: $inputs.override,
          merged: ($inputs.base * $inputs.override)
        }

Glob Documents

Glob documents are named by filename stem:

node:
  configs:
    from_glob: "configs/*.json"   # database.json, cache.json
  result:
    transform:
      jq: |
        {
          db: $configs.database,
          cache: $configs.cache
        }

Transform Node Data

Access intermediate node data:

# Access intermediate node data
$merged

# Access nested fields from intermediate node
$merged.settings.port

Output Format

The jq expression must produce a JSON object. This output is:

  1. Parsed by panconf
  2. Re-encoded to the format specified by the output file extension (JSON, YAML, TOML)
  3. Available to dependent nodes via $nodeName

Examples

Basic Transformation

Extract and rename fields:

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

Deep Merge with jq

Use jq's * operator for recursive merge:

node:
  base:
    from_file: base.yaml
  override:
    from_file: override.yaml
  merged:
    to_file: merged.json
    transform:
      jq: $base * $override

Multi-Document Merge

Merge all documents from a multi-document node:

node:
  configs:
    from_file:
      - defaults.yaml
      - env.yaml
      - local.yaml
  merged:
    to_file: merged.json
    transform:
      jq: $configs.defaults * $configs.env * $configs.local

Array Operations

Transform and filter arrays:

node:
  data:
    from_file: users.json
  summary:
    to_file: summary.json
    transform:
      jq: |
        {
          count: ($data | length),
          names: [$data[].name],
          adults: [$data[] | select(.age >= 18)],
          avgAge: (($data | map(.age) | add) / ($data | length))
        }

Conditional Logic

Use jq's if-then-else:

node:
  config:
    from_file: config.yaml
  processed:
    to_file: processed.json
    transform:
      jq: |
        {
          logLevel: (if $config.debug then "debug" else "info" end),
          secure: ($config.environment == "production"),
          settings: (
            if $config.environment == "production"
            then {ssl: true, timeout: 30}
            else {ssl: false, timeout: 60}
            end
          )
        }

Using Intermediate Nodes

Chain jq with other transforms:

node:
  base:
    from_file: base.yaml
  env:
    from_file: production.yaml

  # Intermediate: merge configs
  merged:
    transform:
      deepMerge:
        - "{{.base}}"
        - "{{.env}}"

  # Final: transform merged data with jq
  final:
    to_file: config.json
    transform:
      jq: |
        {
          config: $merged,
          meta: {
            generated: true,
            keys: ($merged | keys)
          }
        }

Complex Transformations

Restructure nested data:

node:
  services:
    from_file: services.yaml
  manifest:
    to_file: manifest.json
    transform:
      jq: |
        {
          version: "1.0",
          services: [
            $services | to_entries[] | {
              name: .key,
              config: .value,
              enabled: (.value.enabled // true)
            }
          ]
        }

Common jq Functions

Function Example Description
* $a * $b Recursive merge (objects)
+ $a + $b Shallow merge / concatenate
keys $config | keys Get object keys
values $config | values Get object values
length $items | length Array/string length
map(f) $items | map(.name) Transform array elements
select(f) $items[] | select(.active) Filter elements
add $numbers | add Sum array elements
sort_by(f) $items | sort_by(.name) Sort array
group_by(f) $items | group_by(.type) Group array elements
unique $tags | unique Remove duplicates
flatten $nested | flatten Flatten nested arrays
to_entries $obj | to_entries Object to key-value pairs
from_entries $pairs | from_entries Key-value pairs to object
ascii_upcase $name | ascii_upcase Uppercase string
ascii_downcase $name | ascii_downcase Lowercase string
split(s) $csv | split(",") Split string to array
join(s) $items | join(",") Join array to string

For a complete reference, see the jq manual.

Comparison with Other Transforms

Aspect jq gotmpl_inline deepMerge
Merge objects $a * $b Manual Automatic
Filter arrays select(), map() range loops Not supported
Conditionals if-then-else if/else blocks Not supported
String manipulation Limited Full (Sprig) Not supported
Learning curve jq syntax Go templates Simple
Best for Data transformation Text generation Simple merging

Use jq when:

  • You need complex data transformations
  • You're working primarily with JSON-like data structures
  • You want powerful array filtering and mapping
  • You're already familiar with jq

Use gotmpl_inline when:

  • You need text generation or string manipulation
  • You want access to Sprig functions
  • You prefer Go template syntax

Use deepMerge or treeMerge when:

  • You just need to combine configs
  • No transformation is needed

Error Handling

Common errors and their causes:

Error Cause Solution
"failed to parse jq expression" Invalid jq syntax Check expression syntax
"jq execution error" Runtime error (e.g., null access) Use // default for optional fields
"jq expression produced no output" Expression returned nothing Ensure expression produces a value

Handling Optional Fields

# Provide default value if field is null or missing
{
  name: ($config.name // "default"),
  port: ($config.port // 8080)
}

Null Coalescing

# Check if field exists
{
  hasName: ($config.name != null),
  name: ($config.name // "unknown")
}