Comment manipuler du JSON en ligne de commande avec jq

 

jq est un couteau suisse pour lire, filtrer et transformer du JSON en ligne de commande. Il s’intègre parfaitement avec curl, kubectl, docker, des logs JSON, etc.

Objectifs de l’article :

  • Installer jq (Linux/macOS/Windows)
  • Comprendre les bases (filtres, pipe, tableaux)
  • Appliquer 15 cas concrets (extractions, filtres, agrégations, tri, mise à jour, concat, variables…)
  • Connaître les options essentielles (-r, -c, -S, -e, --arg, --argjson)

Installation rapide

  • Debian/Ubuntu: sudo apt install jq
  • Fedora: sudo dnf install jq
  • Arch: sudo pacman -S jq
  • macOS (Homebrew): brew install jq
  • Windows (Scoop): scoop install jq
  • Windows (Chocolatey): choco install jq
  • Docker (sans installer localement): docker run --rm -i imega/jq jq .

Données d’exemple

Nous utiliserons l’exemple JSON ci-dessous (fichier data.json) :

{
  "users": [
    {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"},
    {"id": 2, "name": "Bob",   "active": false, "tags": ["dev"],          "score": 12.1, "created_at": "2025-08-29T10:30:00Z"},
    {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 31.7, "created_at": "2025-09-05T08:45:00Z"}
  ]
}

Les bases de jq

  • Filtre identité : . renvoie l’entrée telle quelle.
  • Accéder à un champ : .users, puis .users[0], .users[].name.
  • Chaîner les filtres : .users[] | select(.active == true) | .name.
  • Construire un objet : {id: .id, label: .name}.
  • Interpolation de chaînes : "\(.name) — id=\(.id)".

Option -r (raw) : sort des chaînes brutes sans guillemets. Très pratique pour des boucles shell.


15 commandes jq qui changent la vie

1) Pretty‑print et validation rapide

cat data.json | jq .            # mise en forme + couleurs
cat data.json | jq -e . >/dev/null && echo OK || echo KO  # -e: code de sortie selon validité

Sortie :

  • Mise en forme du JSON :
    {
    "users": [
      {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"},
      {"id": 2, "name": "Bob",   "active": false, "tags": ["dev"],          "score": 12.1, "created_at": "2025-08-29T10:30:00Z"},
      {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 31.7, "created_at": "2025-09-05T08:45:00Z"}
    ]
    }
    
  • Validation :
    OK
    

Explications :

  • jq . applique le filtre identité pour pretty‑printer le JSON d’entrée.
  • Avec -e, jq renvoie un code de sortie 0 si l’entrée est un JSON valide (d’où “OK”).

  • -C force les couleurs, -M les désactive.

2) Extraire un champ simple

jq -r '.users[0].name' data.json      # Alice
jq -r '.users[] | select(.id==2) | .name' data.json   # Bob

Sortie :

Alice
Bob

Explication :

  • select(.id==2) filtre l’élément voulu, -r supprime les guillemets autour des chaînes.

3) Lister les noms de tous les utilisateurs

jq -r '.users[].name' data.json

Sortie :

Alice
Bob
Chloé

4) Filtrer sur une condition (select)

jq -r '.users[] | select(.active) | .name' data.json   # actifs uniquement
jq '.users[] | select(.score >= 30)' data.json         # objets complets

Sortie :

  • Noms des actifs :
    Alice
    Chloé
    
  • Objets avec score >= 30 :
    {
    "id": 1,
    "name": "Alice",
    "active": true,
    "tags": ["admin", "ops"],
    "score": 42.5,
    "created_at": "2025-09-01T12:00:00Z"
    }
    {
    "id": 3,
    "name": "Chloé",
    "active": true,
    "tags": ["dev", "ops"],
    "score": 31.7,
    "created_at": "2025-09-05T08:45:00Z"
    }
    

Explication :

  • select(expr) laisse passer uniquement les éléments pour lesquels l’expression est vraie.

5) Formater des lignes personnalisées

jq -r '.users[] | "\(.id)\t\(.name)\tactive=\(.active)"' data.json

Sortie :

1	Alice	active=true
2	Bob	active=false
3	Chloé	active=true

Explication : Les interpolations \( ... ) insèrent des valeurs dans une chaîne et -r évite les guillemets et conserve les tabulations.

6) Trier (sort_by) et inverser

jq '.users | sort_by(.score)' data.json
jq -r '.users | sort_by(.created_at) | reverse | .[].name' data.json

Sortie :

  • Tri par score (ascendant) :
    [
    {"id": 2, "name": "Bob",   "active": false, "tags": ["dev"],          "score": 12.1, "created_at": "2025-08-29T10:30:00Z"},
    {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 31.7, "created_at": "2025-09-05T08:45:00Z"},
    {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"}
    ]
    
  • Noms triés par date (récents d’abord) :
    Chloé
    Alice
    Bob
    

Note : reverse inverse l’ordre après le tri croissant par created_at.

7) Sommes, min/max, moyenne

# somme des scores
jq '[.users[].score] | add' data.json
# min / max
jq 'min_by(.users[].score)?' data.json   # pas idéal; préférez:
jq '.users | min_by(.score)' data.json
jq '.users | max_by(.score)' data.json
# moyenne approximative
jq '[.users[].score] | add / length' data.json

Sortie :

  • Somme :
    86.3
    
  • min_by(.users[].score)? (à éviter) :
    null
    
  • min / max corrects :
    {"id": 2, "name": "Bob",   "active": false, "tags": ["dev"],          "score": 12.1, "created_at": "2025-08-29T10:30:00Z"}
    {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"}
    
  • Moyenne (~) :
    28.766666666666666
    

Note : Préférez toujours .users | min_by(.score)/max_by(.score) sur le tableau plutôt que d’essayer d’y accéder depuis la racine.

8) Valeurs uniques (unique, unique_by)

jq '.users | map(.tags) | add | unique' data.json              # tags uniques
jq '.users | unique_by(.active) | map(.name)' data.json        # un par statut

Sortie :

  • Tags uniques :
    ["admin", "dev", "ops"]
    
  • Un nom par statut (actif/inactif) :
    ["Alice", "Bob"]
    

Note : unique/unique_by dédupliquent. Ici, on garde un seul utilisateur par statut actif/inactif.

9) Groupement et comptage (group_by + length)

jq '.users | group_by(.active) | map({active: .[0].active, count: length})' data.json

Sortie :

[{"active": false, "count": 1}, {"active": true, "count": 2}]

Note : group_by regroupe par clé (il trie par la clé avant de grouper).

10) Changer la structure du JSON (projection)

jq '.users | map({id, name, score})' data.json
jq '.users | map({label: .name, meta: {id, active}})' data.json

Sortie :

  • Projection simple :
    [
    {"id":1, "name":"Alice", "score":42.5},
    {"id":2, "name":"Bob",   "score":12.1},
    {"id":3, "name":"Chloé", "score":31.7}
    ]
    
  • Projection imbriquée :
    [
    {"label":"Alice", "meta":{"id":1, "active":true}},
    {"label":"Bob",   "meta":{"id":2, "active":false}},
    {"label":"Chloé", "meta":{"id":3, "active":true}}
    ]
    

Ces projections permettent d’extraire/alléger les payloads pour logs, exports, etc.

11) Mettre à jour des champs (update)

# Augmenter tous les scores de 10%
jq '.users |= map(.score = (.score * 1.10))' data.json
# Marquer Bob actif
jq '.users |= map(if .name=="Bob" then .active=true else . end)' data.json

Sortie :

  • Scores +10% :
    {
    "users": [
      {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 46.75, "created_at": "2025-09-01T12:00:00Z"},
      {"id": 2, "name": "Bob",   "active": false, "tags": ["dev"],          "score": 13.31, "created_at": "2025-08-29T10:30:00Z"},
      {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 34.87, "created_at": "2025-09-05T08:45:00Z"}
    ]
    }
    
  • Bob marqué actif :
    {
    "users": [
      {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"},
      {"id": 2, "name": "Bob",   "active": true,  "tags": ["dev"],          "score": 12.1, "created_at": "2025-08-29T10:30:00Z"},
      {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 31.7, "created_at": "2025-09-05T08:45:00Z"}
    ]
    }
    

Explication : L’opérateur |= met à jour le champ ciblé. Utilisez if/then/else pour modifier conditionnellement.

12) Concaténer/assembler des tableaux (plusieurs fichiers)

# Concat simple (deux fichiers contenant des tableaux JSON)
jq -s 'add' a.json b.json
# Accumuler tous les inputs (flux)
cat a.json b.json | jq -s 'flatten'     # aplatit un niveau

En entrée :

  • a.json = [1, 2]
  • b.json = [3, 4]

Sortie :

  • Concat (-s add) :
    [1, 2, 3, 4]
    
  • flatten :
    [1, 2, 3, 4]
    

Note : -s (slurp) lit tous les fichiers/entrées et crée un tableau d’entrées avant d’appliquer le filtre.

13) Extraire seulement certaines clés

jq '.users | map({id, name})' data.json
jq '.users | map({id, name, tags: (.tags | join(","))})' data.json

Sortie :

  • Sélection de clés :
    [
    {"id":1, "name":"Alice"},
    {"id":2, "name":"Bob"},
    {"id":3, "name":"Chloé"}
    ]
    
  • Avec concaténation des tags :
    [
    {"id":1, "name":"Alice", "tags":"admin,ops"},
    {"id":2, "name":"Bob",   "tags":"dev"},
    {"id":3, "name":"Chloé", "tags":"dev,ops"}
    ]
    

Explication : join(",") fusionne un tableau de chaînes en une seule chaîne.

14) JSON Lines (une ligne par objet) et compact

# Sortie compacte (-c) et une ligne par élément
echo '{"items":[{"id":1},{"id":2}]}' | jq -c '.items[]'
# Repasser en tableau structuré
printf '%s\n' '{"id":1}' '{"id":2}' | jq -s '.'

Sortie :

  • JSONL compact :
    {"id":1}
    {"id":2}
    
  • Slurp (-s) -> tableau :
    [
    {"id": 1},
    {"id": 2}
    ]
    

Astuce : -c est idéal pour logs/streams (une ligne par objet). -s (slurp) recompose un tableau.

15) Variables depuis la CLI (–arg, –argjson)

# --arg crée une variable chaîne
jq -r --arg user "Alice" '.users[] | select(.name==$user) | .id' data.json
# --argjson prend du JSON réel (nombre, booléen, objet)
jq --argjson threshold 30 '.users | map(select(.score > $threshold))' data.json

Sortie :

  • Variable chaîne :
    1
    
  • Variable JSON (seuil=30) :
    [
    {"id": 1, "name": "Alice", "active": true,  "tags": ["admin", "ops"], "score": 42.5, "created_at": "2025-09-01T12:00:00Z"},
    {"id": 3, "name": "Chloé", "active": true,  "tags": ["dev", "ops"],  "score": 31.7, "created_at": "2025-09-05T08:45:00Z"}
    ]
    

Explication :

  • --arg name value passe une chaîne; --argjson name value passe une valeur JSON typée accessible via $name.

Bonus : combiner avec curl, docker, kubectl

# APIs HTTP
curl -s https://api.github.com/repos/owner/repo/issues | jq -r '.[].title'
# Docker (inspect)
docker inspect --format='' fastapi-app | jq
# Kubernetes (objets pods)
kubectl get pods -o json | jq -r '.items[] | "\(.metadata.name)\t\(.status.phase)"'

Voir aussi : Dockeriser une API FastAPI


Options essentielles à connaître

  • -r raw-output : sorties texte sans guillemets (parfait pour scripts)
  • -c compact-output : une ligne par objet (idéal pour logs/JSONL)
  • -S sort-keys : clés triées (diffs plus stables)
  • -M monochrome-output : désactiver les couleurs
  • -e exit-status : code 0 si filtre produit une sortie non nulle/vraie
  • -s slurp : lit toutes les entrées et les met dans un tableau
  • -n null-input : démarre sans entrée (pour générer du JSON de zéro)

Pièges et bonnes pratiques

  • Toujours penser à -r si vous attendez du texte (sinon vous aurez des guillemets).
  • Préférez select(...) plutôt que des if imbriqués quand vous filtrez.
  • Pour de gros volumes, utilisez -c et évitez les pretty‑prints inutiles qui peuvent ralentir jq.
  • Utilisez --arg / --argjson plutôt que de bidouiller des chaînes JSON en shell.
  • Pour déboguer un pipeline, insérez | debug ou . as $x | ... | $x.

Cheatsheet

  • Accéder : .foo, .foo[0], .foo[], .foo.bar? (optionnel)
  • Filtrer : select(.a==1 and .b>0), map(...), any, all
  • Trier : sort_by(.key) | reverse
  • Grouper : group_by(.key) | map({key: .[0].key, count: length})
  • Agréger : add, length, unique, unique_by(.k)
  • Construire : {a: .x, b: (.y | tostring)}
  • Variables : --arg name val, --argjson k '{"a":1}'

Conclusion

jq est indispensable pour trier, filtrer, agréger et reformater du JSON sans écrire un script. Gardez cette page sous la main, et n’hésitez pas à adapter les recettes à vos données.


Pour aller plus loin sur linux