Design Principles

JSON++ was designed around three principles.

Principle 1 — Completeness and Compatibility

Any JSON can be represented as JSON++: An important property of JSON++ is that it does not restrict the space of configurations that can be produced. The jq++ evaluation function maps JSON++ documents to ordinary JSON documents. For any valid JSON document J, there exists a JSON++ document X such that jq++(X) = J. In other words, the evaluation function is surjective onto JSON. This property ensures that JSON++ can generate any configuration expected by downstream systems, allowing jq++ to be introduced as a pre-processing step without reducing expressiveness.

In practice, this means that adopting JSON++ never forces downstream tools to change their expectations about the resulting configuration structure.[1]

All JSON++ documents are valid JSON: Moreover, JSON++ documents remain valid JSON, which allows existing parsers and tooling to process them without modification.

Principle 2 — JSON Data-Model Compatibility

JSON++ operates at the level of the JSON data model rather than a specific surface syntax. Because many configuration languages such as YAML, HCL, and TOML are routinely translated into JSON during processing, the same reuse mechanisms can be applied to configurations written in those formats. In practice this means that JSON++ concepts naturally extend to JSON-compatible representations such as YAML without requiring changes to the authoring language.

This also implies that the same extension concept naturally applies to other JSON-compatible formats such as YAML.

Principle 3 — Declarative Structural Reuse

JSON++ introduces explicit constructs for expressing structural reuse.

  • Structural composition constructs ($extends, $includes) let configurations combine documents directly, describing relationships in the data structure itself rather than through external tooling.

  • Expression evaluation (eval:) lets configurations compute values dynamically from the composed result.

open this page in a window

jq++ Design

This page describes the internal architecture of `jq`, the tool that evaluates JSON configurations. For the design principles behind JSON++, see the Design Principles page.

Evaluation Pipeline

`jq` transforms a JSON configuration file into plain JSON through a deterministic sequence of stages. Each stage reads from the result of the previous one; no stage revisits earlier work.

Diagram

Each stage is described in detail below.

Stage 1 — Parse

The input file is read and converted to the JSON data model. The file format is determined from the file extension. YAML, TOML, JSON5, and HOCON files are decoded by their respective parsers and then normalised into the same map[string]any / []any representation used for JSON internally.

.jq files are a special case: they are parsed as jq source code and registered as a named jq module rather than as a data object. The module name is the base filename without the extension. Any eval: expression in a file that includes such a .jq file via $extends or $includes can call functions defined in that module.

Files with no extension or with .json`, `.yaml, .toml`, `.json5, .conf`, or `.hocon are treated the same as their base format.

Stage 2 — File-level inheritance

$extends and $includes at the root of the current object are resolved. Both directives are handled by the same underlying merge engine; the difference is in merge direction.

Resolving parents

For each filename listed in $extends or $includes, jq` resolves the file against the search path (current file's directory → local node search paths → `JF_PATH`), then recursively applies the full pipeline (stages 1–4) to that parent file. The result is cached in a _node pool_ so that each file is evaluated at most once per `jq invocation, even when it is referenced from multiple places.

Circular inheritance chains are detected by tracking visited absolute paths. If a file that is already being resolved appears again in the chain, an error is reported immediately.

Merge semantics

mergeObjects(parent, child) combines two objects, with the child’s values taking precedence. The merge is recursive for nested objects: if both parent and child have a key whose value is an object, those objects are merged recursively rather than the child object replacing the parent object wholesale. For any other value type (string, number, boolean, array, null), the child value simply replaces the parent value.

Given $extends: [A, B], A is merged over B first, then the current object is merged over that result, giving the priority order: current object → A → B.

Given $includes: [A, B], B is merged over the current-plus-extends result, then A is merged over that, giving the opposite priority order: B → A → current object. In other words, $includes fragments override everything that $extends and the current object establish.

Processing order

$extends is always resolved before $includes, so that $includes can override the combined result.

Stage 3 — Materialize local nodes

Objects defined under the $local key are extracted and written as individual files in a temporary session directory that is created fresh for each top-level jq++ invocation. Each local node name becomes its own file (e.g. BaseThing becomes a file BaseThing with no extension, treated as JSON).

The session directory is placed on the front of the search path for the duration of processing. This is what allows local node names to be used in $extends and $includes by bare name: they resolve against the session directory first. Local node scope is tied to the file that defined them—specifically to the object tree rooted at that file—so local nodes from a parent are visible inside nodes that extend that parent, but not from unrelated sibling nodes.

The $local key is removed from the object before further processing. At the end of the invocation, the session directory is deleted.

Stage 4 — Node-level inheritance

After file-level inheritance is settled, the entire object is scanned for nested objects that themselves contain $extends or $includes keys. These are resolved in outermost-first order: a node at depth 1 is processed before a node at depth 2 that might be contained within it.

The same merge engine, search path, caching, and circular-reference detection used in stage 3 apply here too. Node-level inheritance therefore has exactly the same semantics as file-level inheritance; only the scope differs.

Stage 5 — Key-side evaluation

All keys in the resolved object that start with eval: or raw: are processed.

  • A raw: key has its prefix stripped and is renamed to the remainder.

  • An eval: key is evaluated as a jq expression. If the result is a string, the key is renamed to that string. If the result is an array of strings, the key is replicated once per element, with each copy of the value placed under its respective new key.

Key evaluation may produce new eval: or raw: keys. The stage therefore runs in a loop, decrementing a time-to-live counter (starting at 7) on each pass, until no more such keys remain or the counter reaches zero (which causes a panic indicating a likely infinite loop in the configuration).

Stage 6 — Value-side evaluation

All string values in the object that start with eval: or raw: are evaluated.

  • A raw: value has its prefix stripped; the remaining string is the final value.

  • An eval: value is compiled and executed as a jq expression against the current full object as input. The expression has access to the builtin variables $cur and $curexpr, and to the JSON++ builtin functions (ref, refexpr, reftag, parent, parentof, topatharray, topathexpr, readfile).

An eval: result may itself be a string starting with eval: (for example, when a value is resolved via ref and the referenced value is also an expression). The stage therefore loops, decrementing its own time-to-live counter (starting at 7) on each pass, re-evaluating until no eval: strings remain.

Circular references within expressions—where evaluating value A leads back to value A—are detected via a visited-path map that is threaded through the evaluation call chain, and reported as errors immediately.

Stage 7 — Output

The fully resolved map[string]any object is serialised to JSON with two-space indentation and written to standard output. When multiple input files are provided on the command line, each is processed independently and its output is written in order. When no file arguments are given, jq++ reads from standard input, writing the input to a temporary file first (since the inheritance resolution needs a filesystem path to anchor relative file references).


Node Pool and Caching

The node pool is the shared state that persists across all recursive inheritance resolutions within a single jq++ invocation. Its two main responsibilities are:

Caching. A NodeEntryKey (base directory + filename) maps to a NodeEntryValue (resolved object + accumulated jq modules). The first time a file is requested, it is fully resolved through stages 1–4 and the result is stored. Subsequent requests for the same file return the cached result directly, avoiding redundant work and ensuring consistent results when the same file is extended from multiple places.

Search path management. The node pool maintains a stack of local node directories. When a file is entered for processing, its session-local node directory is pushed onto the stack. When processing returns from that file, the directory is popped. The full search path at any point is: current file’s base directory + local node directory stack + JF_PATH entries.

Merge Object Semantics

mergeObjects(parent, child) performs a deep merge:

  • Keys present only in the parent appear in the result with their parent values.

  • Keys present only in the child appear in the result with their child values.

  • Keys present in both: if both values are objects, they are merged recursively using the same rule; otherwise the child value replaces the parent value entirely.

This means arrays are not merged—a child array replaces the parent array completely. Only nested objects receive the recursive treatment.

open this page in a window

jq++: JSON with structural reuse and expression evaluation

jq++ is a tool that extends your JSON with structural reuse: structural composition (via $extends and $includes) and expression evaluation (via eval:). It productizes the ideas of jq-front, which was named after Cfront[3]—the classic C++-to-C translator—and gives JSON a similar “front-end” power.

Despite criticism of JSON[6] as a format for configuration[4], JSON has clear strengths: broad tool support, a good balance between machine and human readability, and simple, unambiguous syntax.

Where JSON falls short is in structure. When you need many similar but slightly different configurations, redundancy is hard to remove because JSON does not define relationships between files or between nodes inside a file.[1]

What is it good for?

Have you ever had to maintain configuration files that are almost the same, but not quite?

config for foo config for bar
{
  "foo_database": {
    "server": {
      "ip": "192.168.1.5",
      "port": 2001
    },
    "db_name": "foo",
    "user": {
      "name": "root",
      "password": "foo_root"
    }
  }
}
{
  "bar_database": {
    "server": {
      "ip": "192.168.1.5",
      "port": 2001
    },
    "db_name": "bar",
    "user": {
      "name": "root",
      "password": "bar_root"
    }
  }
}

With jq++, you can define a single base JSON for the common parts:

database_default
{
    "server": {
      "ip": "192.168.1.5",
      "port": 2001
    },
    "user": {
      "name": "root"
    }
}

Then reuse it from other files via $extends:

foo.json bar.json
{
  "foo_database": {
    "$extends": [ "database_default.json" ],
    "db_name": "foo",
    "user": {
      "password": "foo_root"
    }
  }
}
{
  "bar_database": {
    "$extends": [ "database_default.json" ],
    "db_name": "bar",
    "user": {
      "password": "bar_root"
    }
  }
}

Run jq foo.json` and you get the expanded “config for foo” from the first table; `jq bar.json gives you the “config for bar”. You keep one source of truth and generate each variant by extending it.

Inheritance merge semantics

When jq++ resolves $extends or $includes, it uses index-wise recursive merge:

  • object values are merged recursively

  • array values are merged by index

  • if both sides have an element at the same index, those elements are merged recursively

  • if only one side has an element at an index, that element is kept

This is especially useful when you want to override only part of an array element without rewriting the entire array.

For example, suppose a parent defines:

parent.json
{
  "spec": {
    "http": [
      {
        "match": [
          {
            "headers": {
              "end-user": {
                "exact": "jason"
              }
            }
          }
        ],
        "fault": {},
        "route": [
          {
            "destination": "v1"
          }
        ]
      },
      {
        "route": [
          {
            "destination": "v2"
          }
        ]
      }
    ]
  }
}

and the child defines:

child.json
{
  "$extends": ["parent.json"],
  "spec": {
    "http": [
      {
        "fault": {
          "abort": {
            "httpStatus": 500
          }
        }
      }
    ]
  }
}

Running jq++ child.json produces:

{
  "spec": {
    "http": [
      {
        "match": [
          {
            "headers": {
              "end-user": {
                "exact": "jason"
              }
            }
          }
        ],
        "fault": {
          "abort": {
            "httpStatus": 500
          }
        },
        "route": [
          {
            "destination": "v1"
          }
        ]
      },
      {
        "route": [
          {
            "destination": "v2"
          }
        ]
      }
    ]
  }
}

The child’s spec.http[0].fault overrides only that part of the first element. The parent’s match, route, and second array element are preserved.

Expression evaluation

jq++ also supports expression evaluation so you can compute values dynamically using jq expressions in string fields.

Example: extend two files and build a greeting string from their fields and the current date:

sayHello.json
{
  "$extends": ["greeting.json", "name.json"],
  "sayHello": "eval:refexpr(\".greeting\") + \", \" + refexpr(\".yourname\") + \". Today is \" + (now|todate)"
}

With greeting.json as {"greeting":"Hello"} and name.json as {"yourname":"Mark"}, running jq++ sayHello.json produces:

{
  "greeting": "Hello",
  "sayHello": "Hello, Mark. Today is 2026-02-14T13:07:57Z",
  "yourname": "Mark"
}

YAML?

JSON is often criticized as a configuration language[4]. Whether or not you agree, jq++ can still improve a YAML-based workflow: you keep YAML for editing, then convert to JSON, run jq++ for structural reuse, and optionally convert back to YAML.

INPUT OUTPUT
$local:
  database_default:
    server:
      ip: 192.168.1.5
      port: 2000
    db_name: test
    user:
      name: root
      password: root

foo_database:
  $extends: [ database_default ]
  server:
    port: 2001
  db_name: foo
  user:
    password: foo_root
foo_database:
  server:
    ip: 192.168.1.5
    port: 2001
  db_name: foo
  user:
    name: root
    password: foo_root

Example pipeline (using yq for YAML↔JSON):[8]

$ yq . -j in.yaml | jq++ | yq . -y

HOCON?

HOCON[hocon] is another human-friendly format with inheritance and references. You might still prefer jq++:

  • Standard JSON in and out. jq++ reads and writes JSON, which fits into almost any toolchain.

  • Custom behavior via expression evaluation. You can define how values are computed using eval: expressions without leaving JSON.

  • Separation of concerns. Data structure (inheritance, references) and human-friendly syntax are different problems; jq++ focuses on structure so you can choose the syntax (JSON, YAML, etc.) separately.

That separation matters in automation and CI/CD. Configs often need to be generated by programs; JSON is easy to generate and parse. HOCON, as a superset of JSON, is less straightforward to emit: a program might produce valid HOCON that looks different from hand-written style after a round trip. If you use jq++ for the “structure and value resolution” step and keep JSON (or YAML) as the interchange format, you avoid mixing style and semantics in one layer.

You can combine jq++ with any JSON-oriented tools—including jq[7] for querying and transformation—and still improve readability by editing in YAML or another format upstream.

open this page in a window

JSON++ Terminology

This document defines the canonical terms used throughout the JSON documentation. When describing JSON behaviour, always prefer these terms over informal alternatives such as templating or rendering.

Structural Reuse

JSON++ provides structural reuse mechanisms that allow configurations to be constructed from reusable fragments. Structural reuse consists of two categories:

  • Structural composition, which combines configuration documents:

    • Inheritance ($extends)

    • Inclusion ($includes)

  • Expression evaluation (eval:), which computes configuration values dynamically.

Structural reuse
 ├─ structural composition
 │   ├─ inheritance ($extends)
 │   └─ inclusion ($includes)
 └─ expression evaluation (eval:)

Structural Composition

Structural composition is the subset of structural reuse that combines configuration documents structurally. JSON++ provides two structural-composition constructs: inheritance ($extends) and inclusion with defaults ($includes).

Inheritance ($extends)

Inheritance is the mechanism by which an object declares that it is a specialisation of one or more parent objects. The current object inherits every field from its parents and may override any of them with its own values.

Precedence direction: base → derived. Parent (base) objects supply defaults; the derived object’s own fields take precedence over all inherited fields. When multiple parents are listed, earlier entries in the $extends array take precedence over later ones.

{
  "$extends": ["base.json", "fallback.json"],
  "key": "this value overrides whatever base.json or fallback.json define"
}

Priority from highest to lowest: current object → first parent → … → last parent.

Inclusion with Defaults ($includes)

Inclusion with defaults is the mechanism by which an object pulls in one or more external fragments that act as authoritative defaults. Unlike inheritance, where the derived object overrides its parents, the included fragments override the including object’s own fields.

Precedence direction: included fragments override the including object. The including object provides initial values; the included fragments are applied on top, overriding those values. When multiple fragments are listed, later entries in the $includes array take precedence over earlier ones.

{
  "$includes": ["platform-defaults.json", "security-policy.json"],
  "log_level": "warn"
}

Priority from highest to lowest: last fragment → … → first fragment → current object’s own fields.

Coexistence with $extends. When both keys are present in the same object, $extends is resolved first and $includes is applied on top. The full priority order from highest to lowest is:

$includes (later entries) → $includes (earlier entries) → current object → $extends (earlier entries) → $extends (later entries)

Expression Evaluation (eval:)

Expression evaluation is the mechanism by which string values or keys prefixed with eval: are replaced by the result of evaluating the suffix as a jq expression. The expression is evaluated against the current JSON root after all structural composition has been resolved.

{
  "version": "eval:string:\"v\" + (.major | tostring)",
  "count":   "eval:number:.items | length"
}

Expression evaluation applies to both values and keys. When a key expression evaluates to an array of strings, one key–value pair is emitted per element.

Escaping Meta Keys (raw:)

Escaping is the mechanism by which a string value or key prefixed with raw: is passed through to the output with the prefix stripped, without any evaluation. This is necessary when a value or key would otherwise be interpreted as an eval: expression or when a key matches a JSON++ reserved word such as $extends, $includes, or $local.

{
  "message":      "raw:eval:this is not an expression",
  "raw:$extends": "this key appears verbatim in the output"
}

open this page in a window

Custom Functions

Custom functions let you define reusable jq logic and call it from eval: expressions anywhere in your configuration. Functions are written in plain jq def syntax and placed in a .jq file. That file is then loaded as a jq module via $extends or $includes.

For the full syntax of $extends and $includes see the Syntax Reference. For available builtin variables and functions see the Builtins page.

Defining a module

Create a file with a .jq extension. Each function is declared with the standard jq def keyword.

A parameterless function:

greet.jq
def hello_world:
  "Hello, world!";

A parameterized function (jq uses ; to separate parameters):

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

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

Functions in a .jq module have full access to jq++ builtins such as ref, refexpr, reftag, parent, and topathexpr. They do not run in a restricted "plain jq only" context, so they can resolve dynamic values directly instead of forcing every such value through explicit parameters.

Loading a module

Reference the .jq file in the $extends or $includes list of your configuration:

{
  "$extends": ["greet.jq"],
  "greeting": "eval:string:greet::hello_world"
}

The file is resolved using the standard search path (current directory first, then JF_PATH). See the Syntax Reference — Search Paths section for details.

Calling a function

Inside an eval: expression, call a custom function using module-qualified syntax:

modulename::functionname

The module name is the filename without the .jq extension. For example, greet.jq becomes the module greet, and utils.jq becomes utils.

input.json — parameterless call
{
  "$extends": ["greet.jq"],
  "greeting": "eval:string:greet::hello_world"
}
input.json — parameterized call
{
  "$extends": ["utils.jq"],
  "apiUrl": "eval:string:utils::url_of(\"api\")"
}

Using jq++ builtins inside functions

Custom functions can call any jq++ builtin. A function wrapping parent to return a path expression:

nav.jq
def current_path:
  parent;
input.json
{
  "$extends": ["nav.jq"],
  "info": {
    "nested": {
      "path": "eval:string:nav::current_path | topathexpr(.)"
    }
  }
}

The expression evaluates to the jq path of the nested object (e.g., .info.nested).

A function can also resolve values from the surrounding jq++ document directly:

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

def tag_value:
  reftag("thetag");
input.json
{
  "$extends": ["funcs.jq"],
  "thetag": "tag-value",
  "ports": { "api": 8080 },
  "resolvedPort": "eval:number:funcs::port_of(\"api\")",
  "resolvedTag": "eval:string:funcs::tag_value"
}

open this page in a window

Expression Evaluation

JSON++ string values and keys that begin with eval: are evaluated as jq expressions. The result replaces the original string in the output.

For precise definitions see the Terminology page. For the full syntax and type annotations see the Syntax Reference. For available builtin variables and functions see the Builtins page.

open this page in a window

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

open this page in a window

Structural Reuse

JSON++ provides structural reuse mechanisms that allow configurations to be constructed from reusable fragments. Structural reuse consists of structural composition ($extends and $includes) and expression evaluation (eval:).

This page focuses on structural composition: the two constructs that combine configuration documents — inheritance ($extends) and inclusion with defaults ($includes).

For precise definitions and precedence rules see the Terminology page. For the full syntax of each construct see the Syntax Reference.

open this page in a window

Builtin variables

jq++ injects the following variables into every eval: expression. They are available as jq variables (e.g. $cur, $curexpr).

$cur

The current path as a path array (JSON array of path segments: object keys as strings, array indices as integers). When evaluating a value (eval: in a value), $cur is the path to that value. When evaluating a key (eval: in a key), $cur is the path to the parent object (the path of the object that contains the key being computed). Use with parent, parentof, ref, or topathexpr to navigate or refer to locations.

$curexpr

The current path as a path expression string (e.g. ".foo.bar[0]"). Available only when evaluating a value; not set when evaluating a key. Useful for debugging or when you need the path in string form.

Builtin Functions

jq++ provides builtin functions that are available inside eval: expressions. Each function is described below with example usage. Examples use the eval:string: or eval:array: prefix so the expression result is used as the value of the key.

Path conversion

topatharray

Signature: topatharray(path_expression_string)

Converts a path expression string (e.g. ".foo.bar[0]") into a path array: a JSON array of path segments (object keys as strings, array indices as integers). Useful for manipulating paths programmatically and passing them to ref or parentof.

Example: examples/builtins/topatharray/input.json

{
  "pathArray": "eval:array:topatharray(\".foo.bar[0]\")"
}

Result: The key pathArray becomes the array corresponding to the path expression:

{
  "pathArray": ["foo", "bar", 0]
}

topathexpr

Signature: topathexpr(path_array)

Converts a path array (slice of keys and indices) into a path expression string (e.g. ".foo.bar[0]"). Non-alphanumeric keys are quoted in bracket notation.

Example: examples/builtins/topathexpr/input.json

{
  "nested": {
    "pathExpr": "eval:string:topathexpr(parent)"
  }
}

Result: The key pathExpr becomes the path expression string for the current parent path:

{
  "nested": {
    "pathExpr": ".nested"
  }
}

Parent path

parent

Signature: parent or parent(level)

Returns the path array of the parent of the current node. With no arguments, goes up one level; with one integer argument, goes up that many levels. Used with the builtin variable $cur (current path array). Typically used as parent or parent(2) inside expressions.

Example: examples/builtins/parent/input.json

{
  "a": {
    "b": {
      "value": "eval:string:topathexpr(parent)"
    }
  }
}

Result: At a.b.value, topathexpr(parent) yields the path to the parent key b:

{
  "a": {
    "b": {
      "value": ".a.b"
    }
  }
}

parentof

Signature: parentof(path_array) or parentof(path_array; level)

Returns the path array with the last segment(s) removed. First argument is a path array; optional second argument is the number of levels to remove (default 1). Often used with $cur to go up from the current path, e.g. parentof($cur; 2) then append a key and pass to ref.

Example: examples/builtins/parentof/input.json

{
  "releaseVersion": "2.0.0",
  "l1": {
    "l2": {
      "snapshotVersion": "eval:string:ref(parentof($cur; 3) + [\"releaseVersion\"]) + \"-SNAPSHOT\""
    }
  }
}

Result: releaseVersion at root is copied into l1.l2.snapshotVersion using parentof($cur; 3) to reach root:

{
  "releaseVersion": "2.0.0",
  "l1": {
    "l2": {
      "snapshotVersion": "2.0.0-SNAPSHOT"
    }
  }
}

Reference and lookup

ref

Signature: ref(path_array)

Returns the value in the current JSON root at the given path array. If that value is a string, it is evaluated as an expression (e.g. another reference or expression). Path array is built from strings (object keys) and integers (array indices). Circular references are detected and reported as errors.

Example: examples/builtins/ref/input.json

{
  "shared": "common value",
  "node": {
    "usesShared": "eval:string:ref([\"shared\"])"
  }
}

Result: The key that uses ref(["shared"]) is replaced by the value at root shared:

{
  "shared": "common value",
  "node": {
    "usesShared": "common value"
  }
}

refexpr

Signature: refexpr(path_expression_string)

Like ref, but takes a path expression string (e.g. ".foo.bar") instead of a path array. Resolves the path from the root and returns the value; if it is a string, that string is evaluated as an expression.

Example: examples/builtins/refexpr/input.json

{
  "source": "original",
  "copy": "eval:string:refexpr(\".source\")",
  "chained": "eval:string:refexpr(\".copy\")"
}

Result: copy and chained resolve to the value of source:

{
  "source": "original",
  "copy": "original",
  "chained": "original"
}

reftag

Signature: reftag(tag_name)

Searches upward from the current path for an object that has a key equal to tag_name, then returns the value at that key. If that value is a string, it is evaluated as an expression. Used to implement “tagged” reference points in the tree.

Example: examples/builtins/reftag/input.json

{
  "thetag": "Hello, mytag",
  "k0": {
    "k1": "eval:string:reftag(\"thetag\")"
  }
}

Result: The key using reftag("thetag") is replaced by the value of thetag from an ancestor object:

{
  "thetag": "Hello, mytag",
  "k0": {
    "k1": "Hello, mytag"
  }
}

File

readfile

Signature: readfile(filename_string)

Resolves the filename relative to the current file’s directory and search paths, reads the file, and parses its contents as JSON. The result is the parsed JSON value (object, array, string, number, boolean, or null). Often used with eval:string: when the file contains a single string to use as a string value.

The filename extension determines how the file is parsed. Supported extensions are: .json, .json`, `.jq`, `.yaml`, `.yml`, `.yaml, .yml`, `.toml`, `.toml, .json5, .json5`, `.conf`, `.hocon`, `.conf, .hocon++. A filename with no extension is treated as JSON. Any other extension causes an error.

Example: examples/builtins/readfile/input.json (and other.json in the same directory).

{
  "key": "eval:string:readfile(\"other.json\")"
}

Result: The key using readfile("other.json") is replaced by the parsed content of other.json (here a string):

{
  "key": "hello"
}

open this page in a window

JSON++ Syntax Reference

JSON++ is a lightweight extension of JSON that adds three mechanisms to standard JSON:

  • structural composition for combining configuration documents (via inheritance with $extends and inclusion with $includes),

  • expression evaluation for computing values dynamically (via eval:), and

  • local node definitions for in-file reuse.

Structural composition and expression evaluation together form structural reuse — the full set of mechanisms for constructing configurations from reusable parts.

All JSON++ documents are valid JSON, so existing JSON tooling continues to work on JSON++ files. The jq++ tool evaluates a JSON++ file and emits ordinary JSON.

Special Keys

JSON++ uses a small set of reserved keys—all prefixed with $—to express structural relationships. These keys are consumed during evaluation and do not appear in the output.

$extends

Declares that the current object inherits from one or more parent objects. Parents are merged in the order listed, and the current object’s own fields override any inherited fields.

Syntax

{
  "$extends": ["parent1.json", "parent2.json"],
  "key": "override-value"
}

The value of $extends must be an array of filename strings. Each filename is resolved relative to the current file’s directory and the JF_PATH search path. Parent files may be in any supported format; the file extension determines how each file is parsed.

Merge order

Given $extends: [A, B], earlier entries in the array take precedence over later ones, and the current object’s own fields take precedence over all parents. In other words, the priority from highest to lowest is: current object → A → B.

Node-level inheritance

$extends can appear at any depth inside an object, not just at the file’s top level.

{
  "database": {
    "$extends": ["db-defaults.json"],
    "db_name": "production"
  }
}

Optional parent files

Append ? to a filename to make it optional. If the file does not exist, it is silently skipped.

{
  "$extends": ["base.json", "overrides.json?"]
}

$includes

Declares that the current object includes one or more external fragments as authoritative defaults. Unlike $extends, where the derived object overrides its parents, $includes inverts the relationship: the current object’s own fields serve as defaults that the included fragments can override.

Syntax

{
  "$includes": ["fragment1.json", "fragment2.json"],
  "key": "value"
}

Fragment files may be in any supported format; the file extension determines how each file is parsed.

Merge order

Given $includes: [A, B], the merge order is the opposite of $extends: later entries in the array take precedence over earlier ones, and the included fragments take precedence over the current object’s own fields. In other words, the priority from highest to lowest is: B → A → current object.

Coexistence with $extends

$extends and $includes can appear in the same object. A useful way to remember their relationship is:

$includes overrides the current object, and the current object overrides $extends.

$extends is resolved first, then $includes is applied on top. The full priority order from highest to lowest is:

$includes (later entries) → $includes (earlier entries) → current object's own fields → $extends (earlier entries) → $extends (later entries)

Tip — building a custom DSL with $includes and JF_PATH

Because $includes fragments override the current object’s own fields, and because files are resolved via JF_PATH, you can use the two features together to build a lightweight domain-specific configuration language where platform-level fragments inject mandatory values.

Define a library of reusable fragments and place them in a directory on JF_PATH. Any configuration file can then pull in exactly the behaviour it needs with a single $includes line, without knowing the absolute location of the library:

{
  "$includes": ["defaults/runtime.json", "defaults/logging.json"],
  "log_level": "warn"
}

Running JF_PATH=/opt/myapp/lib jq++ service.json resolves defaults/runtime.json and defaults/logging.json from the library directory. Switching environments is a matter of changing JF_PATH—the configuration files themselves are untouched.

This pattern lets teams publish versioned "standard" fragments that any project can $includes without copying content. The result is a small but coherent DSL: every configuration file speaks the same vocabulary of shared fragments, and individual files only spell out what makes them different.

$local

Defines named local objects that can be referenced within the same file. Local nodes are materialized as temporary files during evaluation and removed from the output.

Syntax

{
  "$local": {
    "BaseThing": {
      "color": "blue",
      "size": 10
    }
  },
  "thing1": {
    "$extends": ["BaseThing"],
    "size": 20
  },
  "thing2": {
    "$extends": ["BaseThing"],
    "color": "red"
  }
}

Local names are referenced in $extends or $includes by their bare name (without a file extension). The $local key itself is removed from the output.

Expression Evaluation

String values (and string keys) that begin with eval: or raw: receive special treatment.

eval: prefix (values)

A string value starting with eval: is interpreted as a jq expression. The expression is evaluated against the current JSON root, and the result replaces the string.

Syntax

"key": "eval:<expression>"
"key": "eval:<type>:<expression>"

The optional <type> annotation constrains the expected result type. If the result does not match the declared type, evaluation fails with an error.

Supported type annotations

Annotation Expected result type

eval:string:<expr>

String

eval:number:<expr>

Number (integer or float)

eval:bool:<expr>

Boolean

eval:null:<expr>

Null

eval:object:<expr>

JSON object

eval:array:<expr>

JSON array

eval:<expr> (no type)

String (default)

Examples

{
  "version": "eval:string:\"v\" + (.major | tostring)",
  "count":   "eval:number:.items | length",
  "enabled": "eval:bool:.flags.debug",
  "tags":    "eval:array:[\"a\", \"b\"]"
}

If an eval: result is itself a string starting with eval:, it is re-evaluated (up to a fixed iteration limit). Circular references are detected and reported as errors.

eval: prefix (keys)

A key starting with eval: is evaluated and the result is used as the actual key name. If the expression evaluates to an array of strings, multiple keys are created—one per element—each with a copy of the value.

Syntax

{
  "eval:<expression>": "<value>"
}

The expression must evaluate to a string or an array of strings.

Example

{
  "languages": ["en", "ja", "fr"],
  "eval:.languages": {"supported": true}
}

Output:

{
  "languages": ["en", "ja", "fr"],
  "en": {"supported": true},
  "ja": {"supported": true},
  "fr": {"supported": true}
}

When evaluating a key expression, the builtin variable $cur holds the path to the parent object (not to the key itself).

raw: prefix

A string value or key starting with raw: is passed through with the prefix stripped, without any evaluation. This is an escape mechanism for strings that would otherwise be mistaken for eval: expressions.

Syntax (value)

{
  "key": "raw:eval:this is not an expression"
}

Output:

{
  "key": "eval:this is not an expression"
}

Syntax (key)

{
  "raw:eval:literal-key": "value"
}

Output:

{
  "eval:literal-key": "value"
}

Tip — escaping reserved keys

raw: is the only way to produce a literal key (or value) that starts with a JSON reserved prefix such as `$extends`, `$includes`, or `$local`. Because these strings are consumed by jq before output, a bare "$extends" key cannot survive into the final JSON. Prefix it with raw: to pass it through unchanged.

{
  "raw:$extends": "this key appears verbatim in the output",
  "raw:$local":   "so does this one"
}

Output:

{
  "$extends": "this key appears verbatim in the output",
  "$local":   "so does this one"
}

Builtin Variables

The following variables are injected automatically into every eval: expression context.

$cur

The path to the current node, represented as a path array. Each element is a string (object key) or integer (array index). + When evaluating a value, $cur is the path to that value. When evaluating a key, $cur is the path to the parent object. + Example: for a value at .foo.bar[0], $cur is ["foo", "bar", 0].

$curexpr

The path to the current node as a path expression string (e.g. ".foo.bar[0]"). Available only when evaluating a value; not available when evaluating a key.

Builtin Functions

The following functions are available inside eval: expressions.

Path Conversion

Note

A note on path expressions in jq

In standard jq, a path expression (e.g. .foo.bar[0]) is a syntactic construct, not a value. It exists only at the language level—as a fragment of source code—and cannot be stored in a variable, passed as an argument, or returned from a function. Functions such as path/1, getpath/1, and setpath/1 accept path arrays (plain JSON arrays of strings and integers) precisely because path expressions are not first-class data.

JSON++ intentionally departs from this principle. $cur and $curexpr expose the current location as runtime values, and topatharray/topathexpr let you convert between the two representations freely. This is a deliberate usability trade-off: configuration authors need to navigate and reference locations in the document, and string-based path expressions are considerably more readable than raw path arrays when written inside a JSON string.

Treating path expressions as strings is therefore a JSON++-specific convention. It has no meaning to jq itself; the conversion functions bridge the gap when you need to pass a location to a jq built-in such as getpath or setpath.

topatharray(path_expression_string)

Converts a path expression string such as ".foo.bar[0]" into a path array ["foo", "bar", 0].

{
  "result": "eval:array:topatharray(\".foo.bar[0]\")"
}

Output: {"result": ["foo", "bar", 0]}

topathexpr(path_array)

Converts a path array into a path expression string.

{
  "nested": {
    "value": "eval:string:topathexpr(parent)"
  }
}

Output: {"nested": {"value": ".nested"}}

Parent Navigation

parent / parent(level)

Returns the path array of the parent of the current node. Without arguments, goes up one level. With one integer argument n, goes up n levels. Uses the implicit $cur variable.

{
  "a": {
    "b": {
      "c": "eval:string:topathexpr(parent)"
    }
  }
}

Output: {"a": {"b": {"c": ".a.b"}}}

parentof(path_array) / parentof(path_array; level)

Removes the last segment(s) from the given path array. The first argument is any path array; the optional second argument is how many segments to remove (default 1).

{
  "version": "1.0",
  "meta": {
    "deep": {
      "v": "eval:string:ref(parentof($cur; 3) + [\"version\"])"
    }
  }
}

Output: {"version": "1.0", "meta": {"deep": {"v": "1.0"}}}

Reference and Lookup

ref(path_array)

Returns the value in the current JSON root at the given path array. If the value is a string, it is evaluated as an expression (allowing chained references). Circular references are detected and cause an error.

{
  "shared": "common value",
  "node": {
    "copy": "eval:ref([\"shared\"])"
  }
}

Output: {"shared": "common value", "node": {"copy": "common value"}}

refexpr(path_expression_string)

Like ref, but takes a path expression string rather than a path array.

{
  "source": "original",
  "copy": "eval:refexpr(\".source\")"
}

Output: {"source": "original", "copy": "original"}

reftag(tag_name)

Searches upward from the current path for an ancestor object that has a key equal to tag_name, then returns the value at that key. If that value is a string it is evaluated as an expression. Useful for implementing "tagged" reference points in deeply nested trees.

{
  "label": "root-label",
  "section": {
    "item": {
      "inherited": "eval:reftag(\"label\")"
    }
  }
}

Output: {"label": "root-label", "section": {"item": {"inherited": "root-label"}}}

File Operations

readfile(filename_string)

Resolves filename_string relative to the current file’s directory and JF_PATH, reads the file, and parses it as JSON. The result is the parsed JSON value (object, array, string, number, boolean, or null).

{
  "config": "eval:object:readfile(\"defaults.json\")"
}

Supported Input Formats

jq++ accepts files in any of the following formats. The format is determined from the file extension.

Extension Format

.json, .json++

JSON

.yaml, .yml, .yaml`, `.yml

YAML

.toml, .toml++

TOML

.json5, .json5++

JSON5

.hocon, .conf, .hocon`, `.conf

HOCON

.jq

jq module (custom functions available in eval expressions)

YAML, TOML, JSON5, and HOCON files are converted to the JSON data model before processing; the same JSON++ directives work in all formats. The ++ suffix variants (.json`, `.yaml, etc.) are treated identically to their base extensions.

Search Paths

When resolving filenames in $extends, $includes, and readfile(), jq++ searches directories in the following order:

  1. The directory of the file that contains the reference.

  2. Directories listed in the JF_PATH environment variable, separated by : (colon).

This mirrors the behavior of environment variables such as PATH or PYTHONPATH.

$ JF_PATH=/shared/configs:/team/defaults jq++ myconfig.json

See the Design page for a detailed description of the evaluation pipeline.

open this page in a window