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
databaseUrlanddatabasePort -
each service has its own
urlandport -
frontendalso needsbackendUrl
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:
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:
|
|
|
|
Helper functions are defined once:
funcs.jqdef 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.yamlcomponents:
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.yamldatabaseUrl: '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.yamlbackend:
$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