NAME
JQ::Lite - A lightweight jq-like JSON query engine in Perl
VERSION
Version 0.87
SYNOPSIS
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";
}
DESCRIPTION
JQ::Lite is a lightweight, pure-Perl JSON query engine inspired by the 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.
FEATURES
Pure Perl (no XS, no external binaries required)
Dot notation traversal (e.g. .users[].name)
Optional key access using '?' (e.g. .nickname?)
Array indexing and flattening (.users[0], .users[])
Boolean filters via select(...) with ==, !=, <, >, and, or
Pipe-style query chaining using | operator
Built-in functions: length, keys, keys_unsorted, values, first, last, reverse, sort, sort_desc, sort_by, min_by, max_by, unique, unique_by, has, contains, group_by, group_count, join, split, explode, implode, count, empty, type, nth, del, compact, upper, lower, titlecase, abs, ceil, floor, trim, ltrimstr, rtrimstr, substr, slice, startswith, endswith, add, sum, sum_by, avg_by, median_by, product, min, max, avg, median, mode, percentile, variance, stddev, drop, tail, chunks, range, enumerate, transpose, flatten_all, flatten_depth, clamp, to_number, pick, merge_objects, to_entries, from_entries, with_entries
Supports map(...), limit(n), drop(n), tail(n), chunks(n), range(...), and enumerate() style transformations
Interactive mode for exploring queries line-by-line
Command-line interface:
jq-lite
(compatible with stdin or file)Decoder selection via
--use
(JSON::PP, JSON::XS, etc.)Debug output via
--debug
List all functions with
--help-functions
CONSTRUCTOR
new
my $jq = JQ::Lite->new;
Creates a new instance. Options may be added in future versions.
METHODS
run_query
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.
SUPPORTED SYNTAX
.key.subkey
.array[0] (index access)
.array[] (flattening arrays)
.key? (optional key access)
select(.key > 1 and .key2 == "foo") (boolean filters)
group_by(.field) (group array items by key)
group_count(.field) (tally items by key)
sum_by(.field) (sum numeric values projected from each array item)
avg_by(.field) (average numeric values projected from each array item)
median_by(.field) (median of numeric values projected from each array item)
percentile(p) (return the requested percentile for numeric array values)
min_by(.field) / max_by(.field) (select array element with smallest/largest projected value)
sort_desc()
Sort array elements in descending order using smart numeric/string comparison.
Example:
.scores | sort_desc
Returns:
[100, 75, 42, 12]
sort_by(.key) (sort array of objects by key)
unique_by(.key) (remove duplicates based on a projected key)
.key | count (count items or fields)
.[] | select(...) | count (combine flattening + filter + count)
.array | map(.field) | join(", ")
Concatenates array elements with a custom separator string. Example:
.users | map(.name) | join(", ")
Results in:
"Alice, Bob, Carol"
split(separator)
Split string values (and arrays of strings) using a literal separator. Example:
.users[0].name | split("")
Results in:
["A", "l", "i", "c", "e"]
explode()
Convert strings into arrays of Unicode code points. When applied to arrays the conversion happens element-wise, while non-string values (including hashes) are passed through untouched. This mirrors jq's
explode
helper and pairs withimplode
for round-trip transformations.Example:
.title | explode
Returns:
[67, 79, 68, 69]
implode()
Perform the inverse of
explode
by turning arrays of Unicode code points back into strings. Nested arrays are processed recursively so pipelines likeexplode | implode
work over heterogeneous structures. Non-array inputs pass through unchanged.Example:
.codes | implode
Returns:
"CODE"
keys_unsorted()
Returns the keys of an object without sorting them, mirroring jq's
keys_unsorted
helper. Arrays yield their zero-based indices, while non-object/array inputs returnundef
to match the behaviour ofkeys
.Example:
.profile | keys_unsorted
values()
Returns all values of a hash as an array. Example:
.profile | values
pick(key1, key2, ...)
Builds a new object containing only the supplied keys. When applied to arrays of objects, each element is reduced to the requested subset while non-object values pass through unchanged.
Example:
.users | pick("name", "email")
Returns:
[ { "name": "Alice", "email": "alice\@example.com" }, { "name": "Bob" } ]
merge_objects()
Merges arrays of objects into a single hash reference using last-write-wins semantics. Non-object values within the array are ignored. When no objects are found, an empty hash reference is returned. Applying the helper directly to an object returns a shallow copy of that object.
Example:
.items | merge_objects()
Returns:
{ "name": "Widget", "value": 2, "active": true }
to_entries()
Converts objects (and arrays) into an array of entry hashes, each consisting of
key
andvalue
fields in the jq style. Array entries use zero-based index values for the key so they can be transformed uniformly.Example:
.profile | to_entries .tags | to_entries
from_entries()
Performs the inverse of
to_entries
. Accepts arrays containing{ key =
..., value => ... }> hashes or[key, value]
tuples and rebuilds a hash from them. Later entries overwrite earlier ones when duplicate keys are encountered.Example:
.pairs | from_entries
with_entries(filter)
Transforms objects by mapping over their entries with the supplied filter, mirroring jq's
with_entries
. Each entry is exposed as a{ key, value }
hash to the filter, and any entries filtered out are dropped prior to reconstruction.Example:
.profile | with_entries(select(.key != "password"))
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]]
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"
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.
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
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
tail(n)
Returns the final
n
elements of the current array. Whenn
is zero the result is an empty array, and whenn
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
(default0
) and advances bystep
(default1
) until reaching but not includingend
. 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 andvalue
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
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\"}"
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"]
COMMAND LINE USAGE
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'
Interactive Mode
Omit the query to enter interactive mode:
jq-lite data.json
You can then type queries line-by-line against the same JSON input.
Decoder Selection and Debug
jq-lite --use JSON::PP --debug '.users[0].name' data.json
Show Supported Functions
jq-lite --help-functions
Displays all built-in functions and their descriptions.
REQUIREMENTS
Uses only core modules:
JSON::PP
Optional: JSON::XS, Cpanel::JSON::XS, JSON::MaybeXS
SEE ALSO
AUTHOR
Kawamura Shingo <pannakoota1@gmail.com>
LICENSE
Same as Perl itself.