Files
gh-pythoninthegrass-cli-nin…/references/jq-cookbook.md
2025-11-30 08:48:40 +08:00

11 KiB

jq - JSON Processing Cookbook

Overview

jq is a lightweight command-line JSON processor. It's like sed for JSON data.

Basic Queries

Identity and Pretty Print

# Identity (pretty print)
jq . file.json

# Compact output
jq -c . file.json

# Raw output (no quotes for strings)
jq -r . file.json

# Sort keys
jq -S . file.json

Accessing Fields

# Top-level field
jq '.name' file.json

# Nested field
jq '.user.email' file.json

# Deeply nested
jq '.data.user.profile.settings.theme' file.json

# Optional fields (null if missing)
jq '.user.address?' file.json

Array Access

# First element
jq '.[0]' file.json

# Last element
jq '.[-1]' file.json

# Specific index
jq '.[2]' file.json

# Slice (first 3 elements)
jq '.[:3]' file.json

# Slice (elements 2-5)
jq '.[2:5]' file.json

# All elements
jq '.[]' file.json

Iterating

Array Iteration

# Extract field from each element
jq '.users[].name' file.json

# Multiple fields
jq '.users[] | .name, .email' file.json

# Nested iteration
jq '.departments[].employees[].name' file.json

Object Iteration

# All values
jq '.[]' file.json

# All keys
jq 'keys' file.json

# Key-value pairs
jq 'to_entries' file.json

# Iterate key-value
jq 'to_entries[] | .key + ": " + (.value | tostring)' file.json

Filtering

Select

# Filter by condition
jq '.users[] | select(.active == true)' file.json

# Multiple conditions (AND)
jq '.users[] | select(.active == true and .age > 18)' file.json

# Multiple conditions (OR)
jq '.users[] | select(.active == true or .admin == true)' file.json

# Not
jq '.users[] | select(.active != false)' file.json

# Check field exists
jq '.users[] | select(.email != null)' file.json
jq '.users[] | select(has("email"))' file.json

Comparison Operators

# Equality
jq '.items[] | select(.status == "active")' file.json

# Greater/Less than
jq '.items[] | select(.price > 100)' file.json
jq '.items[] | select(.price <= 50)' file.json

# String contains
jq '.items[] | select(.name | contains("widget"))' file.json

# String starts with
jq '.items[] | select(.name | startswith("A"))' file.json

# String ends with
jq '.items[] | select(.name | endswith(".json"))' file.json

# Regex match
jq '.items[] | select(.email | test("@gmail\\.com$"))' file.json

Transforming

Map

# Extract single field
jq '.users | map(.name)' file.json

# Extract multiple fields
jq '.users | map({name, email})' file.json

# Transform values
jq '.prices | map(. * 1.1)' file.json

# Conditional transformation
jq '.users | map(if .active then .name else empty end)' file.json

Reduce

# Sum
jq '[.items[].price] | add' file.json

# Average
jq '[.items[].price] | add / length' file.json

# Min/Max
jq '[.items[].price] | min' file.json
jq '[.items[].price] | max' file.json

# Count
jq '.items | length' file.json

# Custom reduce
jq 'reduce .items[] as $item (0; . + $item.price)' file.json

Group By

# Group by field
jq 'group_by(.category)' file.json

# Group and count
jq 'group_by(.category) | map({category: .[0].category, count: length})' file.json

# Group and aggregate
jq 'group_by(.category) | map({
  category: .[0].category,
  total: map(.price) | add
})' file.json

Sorting

# Sort array
jq 'sort' file.json

# Sort by field
jq 'sort_by(.name)' file.json

# Reverse sort
jq 'sort_by(.name) | reverse' file.json

# Sort by multiple fields
jq 'sort_by(.category, .name)' file.json

# Sort numbers
jq 'sort_by(.price | tonumber)' file.json

Constructing

Objects

# New object with specific fields
jq '{name: .name, email: .email}' file.json

# Rename fields
jq '{username: .name, mail: .email}' file.json

# Computed fields
jq '{
  name: .name,
  fullName: .firstName + " " + .lastName,
  discountPrice: .price * 0.9
}' file.json

# Nested objects
jq '{
  user: {
    name: .name,
    contact: {email: .email}
  }
}' file.json

Arrays

# Create array from fields
jq '[.name, .email, .age]' file.json

# Array of objects
jq '[.users[] | {name, email}]' file.json

# Flatten nested arrays
jq '.departments[].employees | flatten' file.json

# Unique elements
jq '.tags | unique' file.json

# Array difference
jq '.a - .b' file.json

String Operations

# Concatenation
jq '.firstName + " " + .lastName' file.json

# Interpolation
jq '"Hello, \(.name)!"' file.json

# String length
jq '.name | length' file.json

# Uppercase/Lowercase
jq '.name | ascii_upcase' file.json
jq '.name | ascii_downcase' file.json

# Split string
jq '.path | split("/")' file.json

# Join array
jq '.tags | join(", ")' file.json

# Trim whitespace
jq '.text | gsub("^\\s+|\\s+$"; "")' file.json

# Replace
jq '.text | gsub("old"; "new")' file.json

Type Conversions

# To string
jq '.age | tostring' file.json

# To number
jq '.price | tonumber' file.json

# To array
jq '[.]' file.json

# Type check
jq '.value | type' file.json

# Type test
jq 'if .value | type == "string" then "is string" else "not string" end' file.json

Conditionals

# If-then-else
jq 'if .active then "active" else "inactive" end' file.json

# Multiple conditions
jq 'if .score >= 90 then "A"
    elif .score >= 80 then "B"
    elif .score >= 70 then "C"
    else "F"
    end' file.json

# Ternary-like
jq '.users[] | {name, status: (if .active then "active" else "inactive" end)}' file.json

# Alternative operator
jq '.value // "default"' file.json  # Use "default" if .value is null/false

Combining Data

Merging

# Merge objects
jq '. + {newField: "value"}' file.json

# Merge arrays
jq '.a + .b' file.json

# Recursive merge
jq '. * {user: {active: true}}' file.json

# Merge multiple files
jq -s '.[0] + .[1]' file1.json file2.json

Joining

# Array join
jq '.array1 + .array2' file.json

# Unique elements from multiple arrays
jq '(.array1 + .array2) | unique' file.json

# Intersection
jq '.array1 - (.array1 - .array2)' file.json

Advanced Patterns

Working with Nested Data

# Recursive descent
jq '.. | .name? // empty' file.json

# Find all values for a key
jq '.. | .email? // empty' file.json

# Flatten deeply nested structure
jq '[.. | .users? // empty] | flatten' file.json

Multiple Outputs

# Multiple queries
jq '.name, .email' file.json

# Conditional multiple outputs
jq '.users[] | .name, (if .admin then "ADMIN" else empty end)' file.json

Error Handling

# Try-catch
jq 'try .field catch "not found"' file.json

# Optional field access
jq '.user?.email?' file.json

# Default values
jq '.count // 0' file.json

Custom Functions

# Define and use function
jq 'def double: . * 2; .values[] | double' file.json

# Function with parameters
jq 'def multiply(n): . * n; .values[] | multiply(3)' file.json

# Recursive function
jq 'def factorial: if . <= 1 then 1 else . * ((. - 1) | factorial) end; 5 | factorial'

Practical Examples

API Response Processing

# Extract IDs
curl -s 'https://api.example.com/users' | jq '.data[].id'

# Format for CSV
curl -s 'https://api.example.com/users' | \
  jq -r '.data[] | [.id, .name, .email] | @csv'

# Extract nested data
curl -s 'https://api.example.com/posts' | \
  jq '.posts[] | {
    id,
    title,
    author: .user.name,
    comments: .comments | length
  }'

Configuration Management

# Update config value
jq '.database.host = "localhost"' config.json

# Add new field
jq '. + {newFeature: true}' config.json

# Remove field
jq 'del(.deprecated)' config.json

# Merge configs
jq -s '.[0] * .[1]' base.json override.json > merged.json

Data Analysis

# Count by category
jq 'group_by(.category) | map({category: .[0].category, count: length})' data.json

# Top 10 by price
jq 'sort_by(.price) | reverse | .[:10]' data.json

# Statistics
jq '{
  total: length,
  avgPrice: (map(.price) | add / length),
  maxPrice: (map(.price) | max),
  minPrice: (map(.price) | min)
}' data.json

Testing and Validation

# Check required fields
jq '.users[] | select(has("name") and has("email") | not)' data.json

# Validate email format
jq '.users[] | select(.email | test("^[^@]+@[^@]+$") | not)' data.json

# Find duplicates
jq 'group_by(.id) | map(select(length > 1))' data.json

Log Processing

# Filter log level
jq 'select(.level == "error")' logs.json

# Extract error messages
jq 'select(.level == "error") | .message' logs.json

# Group errors by type
jq -s 'group_by(.error_type) | map({type: .[0].error_type, count: length})' logs.json

# Time range filter
jq 'select(.timestamp >= "2024-01-01" and .timestamp < "2024-02-01")' logs.json

Output Formats

# Raw output (no quotes)
jq -r '.name' file.json

# Compact output
jq -c '.' file.json

# Tab-separated
jq -r '.[] | [.name, .age] | @tsv' file.json

# CSV
jq -r '.[] | [.name, .age, .email] | @csv' file.json

# URL encoding
jq -r '.params | @uri' file.json

# Base64 encoding
jq -r '.data | @base64' file.json

# HTML encoding
jq -r '.content | @html' file.json

# JSON output (default)
jq '.' file.json

Command-Line Options

# Read from stdin
echo '{"name":"test"}' | jq .

# Multiple files
jq . file1.json file2.json

# Slurp (read all files into array)
jq -s . file1.json file2.json

# Null input (generate data)
jq -n '{name: "test", date: now}'

# Exit with status code
jq -e 'select(.status == "ok")' file.json

# Tab indentation
jq --tab . file.json

# No color
jq -M . file.json

# Color (force)
jq -C . file.json

Tips and Tricks

Debugging

# Pretty print
jq . file.json | less

# Inspect structure
jq 'paths' file.json

# Show all keys
jq '[paths | select(length == 1)] | unique' file.json

# Debug filter
jq 'debug | .name' file.json

Performance

# Stream processing (memory efficient)
jq --stream . large.json

# Compact input
jq -c . file.json

# Limit output
jq 'limit(10; .items[])' file.json

Combining with Other Tools

# With curl
curl -s 'https://api.github.com/users/github' | jq '.name'

# With fd/rg
fd -e json | xargs jq '.version'

# With fzf
jq -r '.users[].name' users.json | fzf

# With xargs
jq -r '.files[]' manifest.json | xargs cat

Common Patterns

Filter-Map-Reduce

# Classic pattern
jq '.items[] |           # iterate
    select(.active) |    # filter
    .price |             # map
    add'                 # reduce

Pagination

# Page 1 (items 0-9)
jq '.items[0:10]' data.json

# Page 2 (items 10-19)
jq '.items[10:20]' data.json

# Function for pagination
page() {
  local page=$1
  local size=${2:-10}
  local start=$((page * size))
  local end=$((start + size))
  jq ".items[$start:$end]" data.json
}

Data Migration

# Old format to new format
jq 'map({
  id: .user_id,
  profile: {
    name: .username,
    email: .user_email
  }
})' old_format.json > new_format.json

Error Messages

# Common errors and fixes

# "parse error: Invalid numeric literal"
# → Check JSON syntax, trailing commas

# "Cannot iterate over null"
# → Add optional operator: .field[]?

# "Cannot index string with string"
# → Check data types, .field might be string not object

# "jq: error: syntax error"
# → Check quote escaping in shell