JQ::Lite − A lightweight jq−like JSON query engine in Perl
Version 1.49
use JQ::Lite;
my $jq = JQ::Lite−>new;
my @results = $jq−>run_query($json_text,
'.users[].name');
for my $r (@results) {
print encode_json($r), "\n";
}
JQ::Lite is a lightweight, pure−Perl JSON query engine inspired by the jq <https://stedolan.github.io/jq/> command−line tool.
It allows you to extract, traverse, and filter JSON data using a simplified jq−like syntax â entirely within Perl, with no external binaries or XS modules.
|
• |
Pure Perl implementation − no XS or external binaries required. | ||
|
• |
Familiar dot−notation traversal (for example ".users[].name"). | ||
|
• |
Optional key access with "?" so absent keys can be skipped gracefully. | ||
|
• |
Array indexing and flattening helpers such as ".users[0]" and ".users[]". | ||
|
• |
Boolean filters via select(...) supporting comparison operators and logical "and" / "or". | ||
|
• |
Pipe−style query chaining using the "|" operator. | ||
|
• |
Iterator helpers including map(...), map_values(...), walk(...), limit(n), drop(n), tail(n), chunks(n), range(...), and enumerate(). | ||
|
• |
Interactive REPL mode for experimenting with filters line−by−line. | ||
|
• |
Command−line interface ("jq−lite") that reads from STDIN or files. | ||
|
• |
Decoder selection via "−−use" (JSON::PP, JSON::XS, and compatible modules). | ||
|
• |
Debug logging with "−−debug" and a catalog of helpers available through "−−help−functions". |
JQ::Lite mirrors a substantial portion of the jq <https://stedolan.github.io/jq/> function library. Functions are grouped below for easier scanning; every name can be used directly in filters.
|
• |
Structure introspection |
"length", "type", "keys", "keys_unsorted", "values", "paths", "leaf_paths", "to_entries", "from_entries", "with_entries", "map_values", "walk", "arrays", "objects", "scalars", "index", "indices", "rindex".
|
• |
Selection and set operations |
"select", "has", "contains", "any", "all", "unique", "unique_by", "group_by", "group_count", "pick", "merge_objects", "setpath", "getpath", "del", "delpaths", "compact", "drop", "rest", "tail".
|
• |
Transformation helpers |
"map", "map_values", "walk", "enumerate", "transpose", "flatten_all", "flatten_depth", "chunks", "range", "implode", "explode", "join", "split", "slice", "substr", "trim", "ltrimstr", "rtrimstr", "startswith", "endswith", "upper", "lower", "ascii_upcase", "ascii_downcase", "titlecase", "tostring", "tojson", "fromjson", "to_number", "compact".
|
• |
Aggregation and math |
"count", "sum", "sum_by", "add", "product", "min", "max", "min_by", "max_by", "avg", "avg_by", "median", "median_by", "mode", "percentile", "variance", "stddev", "nth", "first", "last", "reverse", "sort", "sort_desc", "sort_by".
|
• |
Flow control and predicates |
"select", "empty", "not", "test", "reduce", "foreach", "if" / "then" / "else" constructs.
my $jq = JQ::Lite−>new;
Creates a new instance. Pass "raw => 1" to enable raw output and "vars => \%hash" to predeclare jq−style variables (for example, "JQ::Lite−>new(vars => { name => 'Alice' })").
my @results = $jq−>run_query($json_text, $query);
Runs a jq−like query against the given JSON string. Returns a list of matched results. Each result is a Perl scalar (string, number, arrayref, hashref, etc.) depending on the query.
|
• |
".key.subkey" to traverse nested hashes. | ||
|
• |
".array[0]" for positional access and ".array[]" to flatten arrays. | ||
|
• |
".key?" for optional key access that yields no output when the key is missing. |
|
• |
"select(.key " 1 and .key2 == "foo")> for boolean filtering. |
|||
|
• |
group_by(.field), group_count(.field), sum_by(.field), avg_by(.field), and median_by(.field) for grouped reductions. |
|||
|
• |
"reduce expr as $var (init; update)" for accumulators with lexical bindings. |
|||
|
• |
"foreach expr as $var (init; update [; extract])" to stream intermediate results while folding values. |
|||
|
• |
unique_by(.key) and sort_by(.key) for deduplicating and ordering complex structures. |
|||
|
• |
".key | count" or ".[] | select(...) | count" to count items after applying filters. |
|||
|
• |
".array | map(.field) | join(", ")" to transform and format array values. |
|
• |
"if CONDITION then FILTER [elif CONDITION then FILTER ...] [else FILTER] end" for jq−style branching. |
Evaluates conditions as filters against the current input. The first truthy branch emits its result; optional "elif" clauses cascade additional tests, and the optional "else" filter only runs when no prior branch matches. Without an "else" clause a fully falsey chain produces no output.
Example:
if .score >=
90 then "A"
elif .score >= 80 then "B"
else "C"
end
|
• |
sort_desc() to order scalars in descending order. |
|||
|
• |
sort_by(.key) for ordering arrays of objects by computed keys. |
|||
|
• |
min_by(.field) / max_by(.field) to select items with the smallest or largest projected values. |
|||
|
• |
percentile(p), "mode", and nth(n) for statistical lookups within numeric arrays. |
|
• |
"join(", ")", split(separator), explode(), and implode() for converting between text and arrays. | ||
|
• |
keys_unsorted() and values() for working with object metadata. | ||
|
• |
paths() and leaf_paths() to enumerate structure paths. | ||
|
• |
getpath(path) and "setpath(path; value)" to read and write by path arrays without mutating the original input. | ||
|
• |
pick(...) and merge_objects() to reshape objects. | ||
|
• |
to_entries(), from_entries(), and with_entries(filter) for entry−wise transformations. | ||
|
• |
map_values(filter) to apply filters across every value in an object or array of objects. |
|
• |
walk(filter) |
Recursively traverses arrays and objects, applying the supplied filter to each value after its children have been transformed, matching jq's "walk/1" behaviour. Arrays and hashes are rebuilt so nested values can be updated in a single pass, while scalars pass directly to the filter.
Example:
.profile | walk(upper)
Returns:
{ "name": "ALICE", "note": "TEAM LEAD" }
|
• |
recurse([filter]) |
Performs a depth−first traversal mirroring jq's "recurse". Each invocation emits the current value and then evaluates either the optional child filter or, when omitted, the object's values and array elements. This makes it easy to walk arbitrary tree structures:
Example:
.users[0] | recurse(.children[]?) | .name
Returns:
"Alice"
"Bob"
"Carol"
|
• |
empty() |
Discards all output. Compatible with jq. Useful when only side effects or filtering is needed without output.
Example:
.users[] | select(.age > 25) | empty
|
• |
.[] as alias for flattening top−level arrays |
|||
|
• |
transpose() |
Pivots an array of arrays from row−oriented to column−oriented form. When rows have different lengths, the result truncates to the shortest row so that every column contains the same number of elements.
Example:
[[1, 2, 3], [4, 5, 6]] | transpose
Returns:
[[1, 4], [2, 5], [3, 6]]
|
• |
flatten_all() |
Recursively flattens nested arrays into a single−level array while preserving non−array values.
Example:
[[1, 2], [3, [4]]] | flatten_all
Returns:
[1, 2, 3, 4]
|
• |
flatten_depth(n) |
Flattens nested arrays up to "n" levels deep while leaving deeper nesting intact.
Example:
[[1, [2]], [3, [4]]] | flatten_depth(1)
Returns:
[1, [2], 3, [4]]
|
• |
arrays |
Emits its input only when the value is an array reference, mirroring jq's "arrays" filter. Scalars and objects yield no output, making it convenient to select array inputs prior to additional processing.
Example:
.items[] | arrays
Returns only the array entries from ".items".
|
• |
objects |
Emits its input only when the value is a hash reference, mirroring jq's "objects" filter. Scalars and arrays yield no output, letting you isolate objects inside heterogeneous streams.
Example:
.items[] | objects
Returns only the object entries from ".items".
|
• |
scalars |
Emits its input only when the value is a scalar (including strings, numbers, booleans, or null/undef), mirroring jq's "scalars" helper. Arrays and objects yield no output, making it easy to focus on terminal values within mixed streams.
Example:
.items[] | scalars
Returns only the scalar entries from ".items".
|
• |
type() |
Returns the type of the value as a string: "string", "number", "boolean", "array", "object", or "null".
Example:
.name | type #
=> "string"
.tags | type # => "array"
.profile | type # => "object"
|
• |
lhs // rhs |
Implements jq's alternative operator. The left−hand side is returned when it produces a defined value (including false or empty arrays); otherwise the right−hand expression is evaluated as a fallback.
Example:
.users[] | (.nickname // .name)
Returns the nickname when present, otherwise the name field.
|
• |
default(value) |
Provides a convenience helper to replace undefined or null pipeline values with a literal fallback.
Example:
.nickname | default("unknown")
Ensures the string "unknown" is emitted when ".nickname" is missing or null.
|
• |
nth(n) |
Returns the nth element (zero−based) from an array.
Example:
.users | nth(0)
# first user
.users | nth(2) # third user
|
• |
del(key) |
Deletes a specified key from a hash object and returns a new hash without that key.
Example:
.profile | del("password")
If the key does not exist, returns the original hash unchanged.
If applied to a non−hash object, returns the object unchanged.
|
• |
delpaths(paths) |
Removes multiple keys or indices identified by the supplied "paths" expression. The expression is evaluated against the current input (for example by using paths() or other jq−lite helpers) and should yield an array of path arrays, mirroring jq's "delpaths/1" behaviour.
Example:
.profile | delpaths([["password"], ["tokens", 0]])
Paths can be generated dynamically using helpers such as paths() before being passed to "delpaths". When a referenced path is missing it is ignored. Providing an empty path ("[]" ) removes the entire input value, yielding "null".
|
• |
compact() |
Removes undef and null values from an array.
Example:
.data | compact()
Before: [1, null, 2, null, 3]
After: [1, 2, 3]
|
• |
upper() |
Converts strings to uppercase. When applied to arrays, each scalar element is uppercased recursively, leaving nested hashes or booleans untouched.
Example:
.title | upper
# => "HELLO WORLD"
.tags | upper # => ["PERL",
"JSON"]
|
• |
titlecase() |
Converts strings to title case (first letter of each word uppercase). When applied to arrays, each scalar element is transformed recursively, leaving nested hashes or booleans untouched.
Example:
.title |
titlecase # => "Hello World"
.tags | titlecase # => ["Perl",
"Json"]
|
• |
lower() |
Converts strings to lowercase. When applied to arrays, each scalar element is lowercased recursively, leaving nested hashes or booleans untouched.
Example:
.title | lower
# => "hello world"
.tags | lower # => ["perl",
"json"]
|
• |
has(key) |
Checks whether the current value exposes the supplied key or index.
* For hashes, returns true when the key is present. * For arrays, returns true when the zero−based index exists.
Example:
.meta |
has("version") # => true
.items | has(2) # => true when at least 3 elements
exist
|
• |
contains(value) |
Checks whether the current value includes the supplied fragment.
* For strings, returns true when the substring exists. * For arrays, returns true if any element equals the supplied value. * For hashes, returns true when the key is present.
Example:
.title |
contains("perl") # => true
.tags | contains("json") # => true
.meta | contains("lang") # => true
|
• |
test(pattern[, flags]) |
Evaluates whether the current string input matches a regular expression. The pattern is interpreted as a Perl−compatible regex, and optional flag strings support the common jq modifiers "i", "m", "s", and "x".
* Scalars are
coerced to strings before matching (booleans become
"true" or
"false"). * Arrays are processed
element−wise, returning arrays of JSON booleans. *
Non−string container values that cannot be coerced
yield "false".
Examples:
.title |
test("ˆHello") # => true
.title | test("world") # => false
(case−sensitive)
.title | test("world"; "i") # => true
(case−insensitive)
.tags | test("ˆp") # => [true, false,
false]
|
• |
all([filter]) |
Evaluates whether every element (optionally projected through "filter") is truthy, mirroring jq's "all/1" helper.
|
• |
For arrays without a filter, returns true when every element is truthy |
(empty arrays yield true).
|
• |
For arrays with a filter, applies the filter to each element and |
requires every produced value to be truthy.
|
• |
When the current input is a scalar, falls back to checking the value's |
truthiness (or the filter's results when supplied).
Examples:
.flags | all #
=> true when every element is truthy (empty => true)
.users | all(.active) # => true when every user is
active
|
• |
any([filter]) |
Returns true when at least one value in the input is truthy. When a filter is provided, it is evaluated against each array element (or the current value when not operating on an array) and the truthiness of the filter's results is used.
* For arrays
without a filter, returns true if any element is truthy. *
For arrays with a filter, returns true when the filter
yields a truthy value
for any element. * For scalars, hashes, and other values,
evaluates the value (or filter results)
directly.
Example:
.flags | any #
=> true when any element is truthy
.users | any(.active) # => true when any user is
active
|
• |
not |
Performs logical negation using jq's truthiness rules. Returns "true" when the current input is falsy (e.g. "false", "null", empty string, empty array, or empty object) and "false" otherwise. Arrays and objects are considered truthy when they contain at least one element or key, respectively.
Examples:
true | not #
=> false
[] | not # => true
.ok | not # => negates the truthiness of .ok
|
• |
unique_by(".key") |
Removes duplicate objects (or values) from an array by projecting each entry to the supplied key path and keeping only the first occurrence of each signature. Use "." to deduplicate by the entire value.
Example:
.users |
unique_by(.name) # => keeps first record for each name
.tags | unique_by(.) # => removes duplicate scalars
|
• |
startswith("prefix") |
Returns true if the current string (or each string inside an array) begins with the supplied prefix. Non−string values yield "false".
Example:
.title |
startswith("Hello") # => true
.tags | startswith("j") # => [false, true,
false]
|
• |
endswith("suffix") |
Returns true if the current string (or each string inside an array) ends with the supplied suffix. Non−string values yield "false".
Example:
.title |
endswith("World") # => true
.tags | endswith("n") # => [false, true,
false]
|
• |
substr(start[, length]) |
Extracts a substring from the current string using zero−based indexing. When applied to arrays, each scalar element receives the same slicing arguments recursively.
Examples:
.title |
substr(0, 5) # => "Hello"
.tags | substr(−3) # => ["erl",
"SON"]
|
• |
slice(start[, length]) |
Returns a portion of the current array using zero−based indexing. Negative start values count from the end of the array. When length is omitted, the slice continues through the final element. Non−array inputs pass through unchanged so pipelines can mix scalar and array values safely.
Examples:
.users |
slice(0, 2) # => first two users
.users | slice(−2) # => last two users
|
• |
rest |
Returns the current array without its first element. Empty arrays yield empty arrays. Non−array inputs pass through untouched so mixed pipelines remain compatible.
Examples:
[1,2,3] | rest
# => [2,3]
[] | rest # => []
|
• |
tail(n) |
Returns the final "n" elements of the current array. When "n" is zero the result is an empty array, and when "n" exceeds the array length the entire array is returned unchanged. Non−array inputs pass through untouched so the helper composes cleanly inside pipelines that also yield scalars or objects.
Examples:
.users |
tail(2) # => last two users
.users | tail(10) # => full array when shorter than
10
|
• |
range(start; end[, step]) |
Emits a numeric sequence that begins at "start" (default 0) and advances by "step" (default 1) until reaching but not including "end". When the step is negative the helper counts downward and stops once the value is less than or equal to the exclusive bound. Non−numeric arguments result in the input being passed through unchanged so pipelines remain resilient.
Examples:
null | range(5)
# => 0,1,2,3,4
null | range(2; 6; 2) # => 2,4
null | range(10; 2; −4) # => 10,6
|
• |
enumerate() |
Converts arrays into an array of objects pairing each element with its zero−based index. Each object contains two keys: "index" for the position and "value" for the original element. Non−array inputs are returned unchanged so the helper composes inside pipelines that may yield scalars or hashes.
Examples:
.users |
enumerate() # =>
[{"index":0,"value":...}, ...]
.numbers | enumerate() | map(.index)
|
• |
index(value) |
Returns the zero−based index of the first occurrence of the supplied value. When the current result is an array, deep comparisons are used so nested structures (hashes, arrays, booleans) work as expected. When the current value is a string, the function returns the position of the substring, or null when not found.
Example:
.users |
index("Alice") # => 0
.tags | index("json") # => 1
|
• |
rindex(value) |
Returns the zero−based index of the final occurrence of the supplied value. Array inputs are scanned from the end using deep comparisons, while string inputs return the position of the last matching substring (or null when not found).
Example:
.users |
rindex("Alice") # => 3
.tags | rindex("perl") # => 2
"banana" | rindex("an") # => 3
|
• |
indices(value) |
Returns every zero−based index where the supplied value appears. For arrays, deep comparisons are performed against each element and the matching indexes are collected into an array. For strings, the helper searches for literal substring matches (including overlapping ones) and emits each starting position. When the fragment is empty, positions for every character boundary are returned to mirror jq's behaviour.
Example:
.users |
indices("Alice") # => [0, 3]
"banana" | indices("an") # => [1, 3]
"perl" | indices("") # => [0, 1, 2,
3, 4]
|
• |
abs() |
Returns absolute values for numbers. Scalars are converted directly, while arrays are processed element−by−element with non−numeric entries preserved.
Example:
.temperature |
abs # => 12
.deltas | abs # => [3, 4, 5, "n/a"]
|
• |
ceil() |
Rounds numbers up to the nearest integer. Scalars and array elements that look like numbers are rounded upward, while other values pass through unchanged.
Example:
.price | ceil #
=> 20
.changes | ceil # => [2, −1, "n/a"]
|
• |
floor() |
Rounds numbers down to the nearest integer. Scalars and array elements that look like numbers are rounded downward, leaving non−numeric values untouched.
Example:
.price | floor
# => 19
.changes | floor # => [1, −2, "n/a"]
|
• |
round() |
Rounds numbers to the nearest integer using standard rounding (half up for positive values, half down for negatives). Scalars and array elements that look like numbers are adjusted, while other values pass through unchanged.
Example:
.price | round
# => 19
.changes | round # => [1, −2, "n/a"]
|
• |
clamp(min, max) |
Constrains numeric values within the supplied inclusive range. Scalars and array elements that look like numbers are coerced into numeric context and clamped between the provided minimum and maximum. When a bound is omitted or non−numeric, it is treated as unbounded on that side. Non−numeric values pass through unchanged so pipelines remain lossless.
Example:
.score |
clamp(0, 100) # => 87
.deltas | clamp(−5, 5) # => [−5, 2, 5,
"n/a"]
|
• |
tostring() |
Converts the current value into a JSON string representation. Scalars are stringified directly, booleans become "true"/"false", and undefined values are rendered as "null". Arrays and objects are encoded to their JSON text form so the output matches jq's behavior when applied to structured data.
Example:
.score |
tostring # => "42"
.profile | tostring # =>
"{\"name\":\"Alice\"}"
|
• |
tojson() |
Encodes the current value as JSON text regardless of its original type, mirroring jq's "tojson". Scalars, booleans, nulls, arrays, and objects all produce a JSON string, allowing raw string inputs to be re−escaped safely for embedding or subsequent decoding.
Example:
.score | tojson
# => "42"
.name | tojson # => "\"Alice\""
.profile | tojson # =>
"{\"name\":\"Alice\"}"
|
• |
fromjson() |
Parses JSON text back into native Perl data structures. Plain strings are decoded directly, while arrays are processed element−by−element to mirror jq's convenient broadcasting behaviour. Invalid JSON inputs are passed through unchanged so pipelines remain lossless.
Example:
.raw | fromjson
# => {"name":"Bob"}
.lines | fromjson # => [1, true, null]
|
• |
to_number() |
Coerces values that look like numbers into actual numeric scalars. Strings are converted with Perl's numeric semantics, booleans become 1 or 0, and arrays are processed element−by−element. Non−numeric strings, objects, and other references are returned unchanged so pipelines remain lossless.
Example:
.score |
to_number # => 42
.strings | to_number # => [10, "n/a", 3.5]
|
• |
trim() |
Removes leading and trailing whitespace from strings. Arrays are processed recursively, while hashes and other references are left untouched.
Example:
.title | trim #
=> "Hello World"
.tags | trim # => ["perl",
"json"]
|
• |
ltrimstr("prefix") |
Removes "prefix" from the start of strings when present. Arrays are processed recursively so nested string values receive the same treatment. Inputs that do not begin with the supplied prefix are returned unchanged.
Example:
.title |
ltrimstr("Hello ") # => "World"
.tags | ltrimstr("#") # => ["perl",
"json"]
|
• |
rtrimstr("suffix") |
Removes "suffix" from the end of strings when present. Arrays are processed recursively so nested string values are handled consistently. Inputs that do not end with the supplied suffix are returned unchanged.
Example:
.title |
rtrimstr(" World") # => "Hello"
.tags | rtrimstr("ing") # => ["perl",
"json"]
"jq−lite" is a CLI wrapper for this module.
cat data.json |
jq−lite '.users[].name'
jq−lite '.users[] | select(.age > 25)' data.json
jq−lite −r '.users[].name' data.json
jq−lite '.[] | select(.active == true) | .name'
data.json
jq−lite '.users[] | select(.age > 25) | count'
data.json
jq−lite '.users | map(.name) | join(", ")'
jq−lite '.users[] | select(.age > 25) | empty'
jq−lite '.profile | values'
Omit the query to enter interactive mode:
jq−lite data.json
You can then type queries line−by−line against the same JSON input.
jq−lite −−use JSON::PP −−debug '.users[0].name' data.json
jq−lite −−help−functions
Displays all built−in functions and their descriptions.
Uses only core modules:
|
• |
JSON::PP |
Optional: JSON::XS, Cpanel::JSON::XS, JSON::MaybeXS
JSON::PP, jq <https://stedolan.github.io/jq/>
The project homepage provides documentation, examples, and release notes: <https://kawamurashingo.github.io/JQ−Lite/index−en.html>.
Kawamura Shingo <[email protected]>
Same as Perl itself.