JQ::Lite(3)
π NAME π
Okay bestie, welcome to JQ::Lite(3) β that's the manual name β from the User Contributed Perl Documentation. This page is all about JQ::Lite β jq-compatible JSON query engine in pure Perl (no external binaries). No cap, that's the whole vibe. π
πΈ VERSION πΈ
Version 2.45. That's the number, periodt.
β¨ SYNOPSIS β¨
Here's the little main-character snippet:
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, jq-compatible alternative JSON processor written in pure Perl. It's literally designed for environments where the jq binary (https://stedolan.github.io/jq/) cannot be installed β think minimal Linux distributions, restricted servers, legacy systems, containers, CI pipelines, or air-gapped environments. π«Ά
JQ::Lite provides a familiar jq-like query syntax for extracting, traversing, and filtering JSON data, while requiring no external binaries, no XS modules, and no compilation step. Iconic.
The module can be used both as a Perl library and via the bundled command-line tool jq-lite, offering long-term CLI stability suitable for OS-level packaging. It's giving "built for real deployments." π
π USE CASES π
- jq is not available or cannot be installed
- Minimal or embedded Linux environments (e.g. Alpine Linux)
- Containers and CI systems without package managers
- Legacy systems with old or restricted toolchains
- Offline or security-sensitive (air-gapped) deployments
π₯ FEATURES π₯
Core capabilities
- Pure Perl implementation β no XS, no native libraries, no external binaries.
- jq-compatible dot-notation traversal β for example
.users[].name. - Optional key access using
?β safely skip missing fields. - Array indexing and flattening helpers β such as
.users[0]and.users[]. - Boolean filtering with
select(...)β supports comparison operators and logicaland/orexpressions. - Pipe-style query chaining using the
|operator β similar to jq. - Iterator and transformation helpers β including
map(...),map_values(...),walk(...),limit(n),drop(n),tail(n),chunks(n),range(...), andenumerate(). - Interactive REPL mode β experiment with jq-like filters line by line.
- Command-line interface
jq-liteβ reads JSON from STDIN or files. - Decoder selection via
--useβJSON::PP,JSON::XS, and compatible modules. - Debug logging with
--debugβ and discoverable helpers via--help-functions.
Built-in 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. iykyk. π
- 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/elseconstructs.
π CONSTRUCTOR π
new
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' })). Obsessed with how easy that is.
π§ 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. It's giving "results, bestie."
π SUPPORTED SYNTAX π
Navigation
.key.subkeyto 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.
Filtering and projection
- Boolean filtering:
"select(.key " 1 and .key2 == "foo")>. group_by(.field),group_count(.field),sum_by(.field),avg_by(.field), andmedian_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)andsort_by(.key)for deduplicating and ordering complex structures..key | countor.[] | select(...) | countto count items after applying filters..array | map(.field) | join(", ")to transform and format array values.
Conditionals
if CONDITION then FILTER [elif CONDITION then FILTER ...] [else FILTER] endfor 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. No output is still a vibe, technically.
Example:
if .score >= 90 then "A"
elif .score >= 80 then "B"
else "C"
end
Sorting and ranking
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, andnth(n)for statistical lookups within numeric arrays.
String and collection utilities
join(", "),split(separator),explode(), andimplode()for converting between text and arrays.keys_unsorted()andvalues()for working with object metadata.paths()andleaf_paths()to enumerate structure paths.getpath(path)andsetpath(path; value)to read and write by path arrays without mutating the original input.pick(...)andmerge_objects()to reshape objects.to_entries(),from_entries(), andwith_entries(filter)for entry-wise transformations.map_values(filter)to apply filters across every value in an object or array of objects.
Detailed helper reference
walk(filter)β Recursively traverses arrays and objects, applying the supplied filter to each value after its children have been transformed, matching jq'swalk/1behaviour. 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'srecurse. 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
.[]β 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 tonlevels 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'sarraysfilter. 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'sobjectsfilter. 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'sscalarshelper. 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, ornull.
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)default(value)β the safety net bestie π«Ά it replaces undefined or null pipeline values with a literal fallback so your pipeline never ghosts you.
Example:
.nickname | default("unknown")
That guarantees the string "unknown" gets emitted whenever .nickname is missing or null. Obsessed, honestly π
nth(n)β grabs the nth element out of an array. Zero-based, so0is the first one, like a civilized human being.
Example:
.users | nth(0) # first user
.users | nth(2) # third user
del(key)β yeets a specified key out of a hash object and hands you back a new hash without that key. Original stays unbothered.
Example:
.profile | del("password")
If the key isn't even there, you get the original hash back, unchanged β¨
If you aim it at a non-hash object? It just returns the object unchanged. No drama. π¦
delpaths(paths)β the group-chat version ofdel: removes multiple keys or indices, all identified by thepathsexpression you feed it. That expression is evaluated against the current input (think: by usingpaths()or other jq-lite helpers) and it should yield an array of path arrays, mirroring jq'sdelpaths/1behaviour fr fr.
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 β no error, no tantrum. And providing an empty path ([]) removes the entire input value, yielding null. So likeβ¦ be careful with that one, bestie π
compact()β clears out undef and null values from an array. The glow-up is real.
Example:
.data | compact()
Before: [1, null, 2, null, 3]
After: [1, 2, 3]
upper()β converts strings to uppercase. When you point it at an array, each scalar element gets uppercased recursively, leaving nested hashes or booleans untouched (they're not invited to this party π).
Example:
.title | upper # => "HELLO WORLD"
.tags | upper # => ["PERL", "JSON"]
titlecase()β converts strings to title case, i.e. first letter of each word uppercase. On arrays, each scalar element gets transformed recursively, leaving nested hashes or booleans untouched.
Example:
.title | titlecase # => "Hello World"
.tags | titlecase # => ["Perl", "Json"]
lower()β converts strings to lowercase. On arrays, each scalar element gets lowercased recursively, leaving nested hashes or booleans untouched.
Example:
.title | lower # => "hello world"
.tags | lower # => ["perl", "json"]
has(key)β the vibe check: does the current value actually expose the key or index you asked about? π- 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 fragment you handed it π- 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
contains_subset(value)β the opt-in jq-style subset containment. Behaves likecontains/1, but when arrays are involved the right-hand array must be a multiset subset of the left-hand one. Order does not matter and duplicate elements are honoured (yes, they count). Nested arrays inside hashes get evaluated with the same subset rules. It's giving "math but make it fashion" πΈ
Examples:
[1,2,3] | contains_subset([2,3]) # => true
[1,2,3] | contains_subset([3,2]) # => true
[1,2,3] | contains_subset([4]) # => false
{"b":{"y":[1,2,3]}} | contains_subset({"b":{"y":[2]}}) # => true
test(pattern[, flags])β asks the question: does the current string input match this regular expression? The pattern is interpreted as a Perl-compatible regex, and the optional flag string supports the common jq modifiersi,m,s, andx.- 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.
- Scalars are coerced to strings before matching (booleans become
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])β checks whether every element (optionally projected throughfilter) is truthy, mirroring jq'sall/1helper. It's the strict friend who needs everyone on board π€- 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's evaluated against each array element (or the current value when you're not operating on an array) and the truthiness of the filter's results is what counts. One icon is enough to carry the group π«Ά- 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β logical negation using jq's truthiness rules. Returnstruewhen the current input is falsy (e.g.false,null, empty string, empty array, or empty object) andfalseotherwise. Arrays and objects are considered truthy when they contain at least one element or key, respectively. Flip the vibe β¨
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 onto the supplied key path, keeping only the first occurrence of each signature. Use.to deduplicate by the entire value. First come, first served π
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 yieldfalse, no exceptions, no pity π
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 yieldfalse.
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. Snipped and snatched βοΈ
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. Negativestartvalues count from the end of the array (iconic behaviour). Whenlengthis 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 (no errors, no theatrics). Non-array inputs pass through untouched so mixed pipelines remain compatible π¦
Examples:
[1,2,3] | rest # => [2,3]
[] | rest # => []
tail(n)β returns the finalnelements of the current array. Whennis zero the result is an empty array, and whennexceeds 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. All about that last-bits energy β¨
Examples:
.users | tail(2) # => last two users
.users | tail(10) # => full array when shorter than 10
chunks(n)β splits the current array into sub-arrays of lengthn. A size of zero is rounded up to one so you always receive at least single-element chunks (it refuses to give you nothing, king behaviour π). When the final partition would be shorter thann, the trailing chunk contains only the remaining elements. Non-array inputs are returned untouched so mixed pipelines continue working as expected.
Examples:
[1,2,3,4,5] | chunks(2) # => [[1,2],[3,4],[5]]
[1,2,3] | chunks(0) # => [[1],[2],[3]]
range(start; end[, step])β emits a numeric sequence that begins atstart(default0) and advances bystep(default1) until it reaches but does not includeend. 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. It's giving countdown π
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:indexfor the position andvaluefor the original element. Non-array inputs are returned unchanged so the helper composes inside pipelines that may yield scalars or hashes. Numbered, labelled, organised β main character energy π
Examples:
index(value)β gives you the zero-based index of the first time that value shows up β¨. When you're working with an array it does deep comparisons, so nested stuff (hashes, arrays, booleans) matches the way you'd expect β no cap. When the current thing is a string instead, it returns the position of the substring, ornullif it's not in there anywhere π
.users | index("Alice") # => 0
.tags | index("json") # => 1
rindex(value)β same vibe asindexbut from the back π¦. It gives you the zero-based index of the final occurrence. Arrays get scanned from the end using deep comparisons, and strings give you the position of the last matching substring (ornullwhen there's no match at all) π
.users | rindex("Alice") # => 3
.tags | rindex("perl") # => 2
"banana" | rindex("an") # => 3
indices(value)β the full receipts π: every zero-based index where the value appears. For arrays it runs deep comparisons against each element and collects the matching indexes into an array. For strings it hunts for literal substring matches (including overlapping ones) and drops each starting position. And when the fragment is empty, it returns positions for every character boundary, mirroring jq's behaviour exactly π
.users | indices("Alice") # => [0, 3]
"banana" | indices("an") # => [1, 3]
"perl" | indices("") # => [0, 1, 2, 3, 4]
abs()β absolute values for numbers, periodt πͺ. Scalars get converted directly, while arrays are processed element-by-element with non-numeric entries preserved (they stay exactly as they were, bestie)
.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 get rounded upward, while everything else passes through untouched πΈ
.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 go downward, and non-numeric values are left alone, no notes π
.price | floor # => 19
.changes | floor # => [1, -2, "n/a"]
round()β nearest integer, standard rounding rules: half up for positive values, half down for negatives π―. Scalars and array elements that look like numbers get adjusted, other values pass through unchanged π«Ά
.price | round # => 19
.changes | round # => [1, -2, "n/a"]
clamp(min, max)β keeps your numeric values inside the inclusive range you hand it π§. Scalars and array elements that look like numbers get coerced into numeric context and clamped between the supplied minimum and maximum. If a bound is omitted or isn't numeric, that side is treated as unbounded π. Non-numeric values pass through unchanged so your pipelines stay lossless β we love a respectful queen
.score | clamp(0, 100) # => 87
.deltas | clamp(-5, 5) # => [-5, 2, 5, "n/a"]
tostring()β turns the current value into a JSON string representation β¨. Scalars are stringified directly, booleans become"true"/"false", and undefined values render as"null". Arrays and objects get encoded to their JSON text form, so the output matches jq's behavior on structured data β
.score | tostring # => "42"
.profile | tostring # => "{\"name\":\"Alice\"}"
tojson()β encodes the current value as JSON text no matter what type it started as, mirroring jq's"tojson"π. Scalars, booleans, nulls, arrays and objects all produce a JSON string, which means raw string inputs can be re-escaped safely for embedding or for decoding later π
.score | tojson # => "42"
.name | tojson # => "\"Alice\""
.profile | tojson # => "{\"name\":\"Alice\"}"
fromjson()β parses JSON text back into native Perl data structures π¦. Plain strings get decoded directly, while arrays are processed element-by-element to mirror jq's handy broadcasting behaviour. Invalid JSON inputs are passed through unchanged so nothing gets lost along the way π
.raw | fromjson # => {"name":"Bob"}
.lines | fromjson # => [1, true, null]
to_number()β coerces values that look like numbers into actual numeric scalars π’. Strings are converted using Perl's numeric semantics, booleans become1or0, and arrays are processed element-by-element. Non-numeric strings, objects and other references come back unchanged so your pipeline stays lossless π
.score | to_number # => 42
.strings | to_number # => [10, "n/a", 3.5]
trim()β swipes the leading and trailing whitespace off strings π§Ή. Arrays are processed recursively, while hashes and other references are left untouched π
.title | trim # => "Hello World"
.tags | trim # => ["perl", "json"]
ltrimstr("prefix")β removes"prefix"from the start of strings when it's actually there π. Arrays are processed recursively so nested string values get the same treatment. Inputs that don't begin with the supplied prefix come back unchanged πΈ
.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 don't end with the supplied suffix come back unchanged π
.title | rtrimstr(" World") # => "Hello"
.tags | rtrimstr("ing") # => ["perl", "json"]
π₯ COMMAND LINE USAGE π₯
"jq-lite" is a CLI wrapper for this module, bestie β here's the whole menu π
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 and you drop straight into interactive mode:
jq-lite data.json
Then you can just type queries line-by-line against the same JSON input π«Ά it's giving live coding session, honestly.
π΅οΈ 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 β a whole glossary, no gatekeeping π
π§Έ REQUIREMENTS π§Έ
Uses only core modules:
JSON::PP
Optional: JSON::XS, Cpanel::JSON::XS, JSON::MaybeXS
π SEE ALSO π
JSON::PP, jq β https://stedolan.github.io/jq/
π HOMEPAGE π
The project homepage provides documentation, examples, and release notes: https://kawamurashingo.github.io/JQ-Lite/index-en.html.
βοΈ AUTHOR βοΈ
Shingo Kawamura β pannakoota1@gmail.com
π LICENSE π
Same as Perl itself.
And that's the whole page, bestie π this was written for perl v5.42.2 and dated 2026-05-08 β so if your Perl is older than that whole era, maybe go update before you come for me π
β¨ slay, and happy querying π¦π
π¦ where this page comes from
- alpine 3.24: perl-jq-lite-doc 2.45-r0 Β· GPL-1.0-or-later OR Artistic-1.0-Perl
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms