This example shows why jq++ is useful when your configuration has repeated structure and cross-references. You keep small YAML fragments as source of truth, then let jq++ resolve inheritance and references.

Problem

Suppose a system has several components (backend, admin, frontend) that share common fields:

  • each service needs databaseUrl and databasePort

  • each service has its own url and port

  • frontend also needs backendUrl

Without jq++, these values are copied around and can drift over time.

If you don’t use jq++

Without jq++, you typically maintain a fully expanded file by hand. That means the same values are repeated in multiple places:

Hand-written expanded configuration
components:
  admin:
    databasePort: 5432
    databaseUrl: postgres
    port: 8081
    type: admin
    url: admin
  backend:
    databasePort: 5432
    databaseUrl: postgres
    port: 8080
    type: backend
    url: backend
  frontend:
    backendUrl: backend
    databasePort: 5432
    databaseUrl: postgres
    port: 3000
    type: frontend
    url: frontend
ports:
  admin: 8081
  backend: 8080
  database: 5432
  frontend: 3000
urls:
  admin: admin
  backend: backend
  database: postgres
  frontend: frontend

This works, but has common maintenance risks:

  • Updating one value requires editing several places

  • Copy/paste mistakes can leave inconsistent values between sections

  • It is hard to tell what is "source of truth" vs derived data

  • Reviewing diffs is noisy because one intent can touch many lines

With jq++, you edit compact source files and regenerate this expanded view deterministically.

Source files (YAML)

The main file defines reusable structure and computed values:

system.yaml
$extends:
  - funcs.jq
ports:
  $includes:
    - ports.yaml
urls:
  $includes:
    - urls.yaml
$local:
  baseService:
    databaseUrl: 'eval:string:funcs::url_of("database")'
    databasePort: 'eval:number:funcs::port_of("database")'
    url: "eval:string:funcs::url_of(funcs::type)"
    port: "eval:number:funcs::port_of(funcs::type)"
components:
  backend:
    $extends:
      - baseService
    type: backend
  admin:
    $extends:
      - baseService
    type: admin
  frontend:
    $extends:
      - baseService
    type: frontend
    backendUrl: 'eval:string:funcs::url_of("backend")'

Lookup tables are kept in separate files:

ports.yaml

urls.yaml

backend: 8080
admin: 8081
frontend: 3000
database: 5432
backend: backend
admin: admin
frontend: frontend
database: postgres

Helper functions are defined once:

funcs.jq
def port_of($service):
  refexpr(".ports.\($service)");

def url_of($service):
  refexpr(".urls.\($service)");

def type:
  ref(parent + ["type"]);

In this version, funcs::type reads the component type from the current level. That allows url and port to live in baseService (one level up), so each component only declares its identity and unique fields.

Run pipeline

Use yq to convert YAML to JSON before and after jq++:

yq . -j system.yaml > system.json
jq++ input.json > expanded.json
yq . -y expanded.json > expanded.yaml

Or as a one-liner:

yq . -j system.yaml | jq++ | yq . -y

Result

jq++ produces fully materialized configuration from compact source files:

expected.yaml
components:
  admin:
    databasePort: 5432
    databaseUrl: postgres
    port: 8081
    type: admin
    url: admin
  backend:
    databasePort: 5432
    databaseUrl: postgres
    port: 8080
    type: backend
    url: backend
  frontend:
    backendUrl: backend
    databasePort: 5432
    databaseUrl: postgres
    port: 3000
    type: frontend
    url: frontend
ports:
  admin: 8081
  backend: 8080
  database: 5432
  frontend: 3000
urls:
  admin: admin
  backend: backend
  database: postgres
  frontend: frontend

Advanced topic: split into four independent concerns

Once the basic version is clear, you can modularize it further. In addition to ports.yaml and urls.yaml, you can externalize:

  • service defaults (base-service.yaml)

  • component definitions (components.yaml)

This keeps each concern in its own file and makes larger systems easier to evolve.

Main wiring file:

system.yaml (advanced)
$extends:
  - funcs.jq
ports:
  $includes:
    - ports.yaml
urls:
  $includes:
    - urls.yaml
components:
  $includes:
    - components.yaml

Service defaults:

base-service.yaml
databaseUrl: 'eval:string:funcs::url_of("database")'
databasePort: 'eval:number:funcs::port_of("database")'
url: "eval:string:funcs::url_of(funcs::type)"
port: "eval:number:funcs::port_of(funcs::type)"

Component declarations:

components.yaml
backend:
  $extends:
    - base-service.yaml
  type: backend
admin:
  $extends:
    - base-service.yaml
  type: admin
frontend:
  $extends:
    - base-service.yaml
  type: frontend
  backendUrl: 'eval:string:funcs::url_of("backend")'

The generated result is the same as the basic example:

expected.yaml (advanced)
components:
  admin:
    databasePort: 5432
    databaseUrl: postgres
    port: 8081
    type: admin
    url: admin
  backend:
    databasePort: 5432
    databaseUrl: postgres
    port: 8080
    type: backend
    url: backend
  frontend:
    backendUrl: backend
    databasePort: 5432
    databaseUrl: postgres
    port: 3000
    type: frontend
    url: frontend
ports:
  admin: 8081
  backend: 8080
  database: 5432
  frontend: 3000
urls:
  admin: admin
  backend: backend
  database: postgres
  frontend: frontend

Why this is useful

  • Reduce duplication: shared fields live in one place ($local.baseService)

  • Separate concerns: lookup data (ports.yaml, urls.yaml) is independent from composition (system.yaml)

  • Keep YAML editing workflow while gaining inheritance and expression evaluation

  • Generate deterministic expanded config for CI/CD and deployment