配列やnull(特定のキーがない)があるJSONからjqでTSV作ってみた

jqで配列のjoin、if文までできるとは
2020.11.16

jqを使ってJSONからTSVを作りたかったのですが、やり始めてから

このJSON、中に配列持ってるわ...。 あ、このkeyを持ってないレコードもあるし...。

と気付きました。 JSONは単なるKeyValueに限らず、Valueとして配列なども持つことができますし、 ファイルに含まれているレコードが全て同じKey項目を持っていることなどもちろん保証できません。 この辺を考慮できないと2次元のTSVに落とすことはできません。 最初は諦めてPythonのコードを書こうかと思ったのですが、 調べてみるとjqなら難なくできることがわかりました!

今回は、配列は単純にカンマ区切りの文字列にできるものとして、 配列を含むJSONからTSVを作る処理を行ってみました。

具体的には以下のようなJSONから

test.json

[
    {
        "job": {
            "jobId": "sample_job_01",
            "targets": [ "aaa", "bbb", "ccc" ]
        }
    },
    {
        "job": {
            "jobId": "sample_job_02"
        }
    },
    {
        "job": {
            "jobId": "sample_job_03",
            "targets": []
        }
    }
]

以下のような出力を得たいという要件です。

sample_job_01	aaa,bbb,ccc
sample_job_02	
sample_job_03

2,3行目の2カラム目には空文字を入れるようにします。

基本のおさらい

まずは基本から始めます。 特に何の変哲もなく、文字列が格納されている場合以下のようにできます。

test.json

[
    {
        "job": {
            "jobId": "sample_job_01",
            "targets": "aaa,bbb,ccc"
        }
    }
]
$ cat test.json | jq -r '.[] | .job | [.jobId, .targets] | @tsv'
sample_job_01   aaa,bbb,ccc

最後に@tsvをするために、[.jobId, .targets]で配列にする所がポイントですね。

配列を文字列に直す

次にtargetsを配列にしてみます。

test.json

[
    {
        "job": {
            "jobId": "sample_job_01",
            "targets": ["aaa","bbb","ccc"]
        }
    }
]

連想配列オブジェクトの中に配列が入っているパターンです。 今回はコンマ区切りの文字列として出力することを目的としています。 この場合は以下のようにやればOKでした。

$ cat test.json | jq -r '.[] | .job | .targets |= join(",") | [.jobId, .targets] | @tsv'
sample_job_01   aaa,bbb,ccc

ポイントは.targets |= join(",")です。 =による代入は.targets = "aaa"のように文字列を代入などはできますが、 今回は.targetsを展開させたいので、Update-assignmentである|=を使用します。 これは「.number |= +1のように書ける」と言えばどんな挙動のものかはすぐにわかるかと思います。 join(",")はおよそ見た通り、配列を,で結合して文字列にする命令です。 これらを組み合わせることで、配列を文字列に変換できました。

nullがあるパターン

では最初に挙げた、.targetsが省略されているものもあるパターンです。

test.json

[
    {
        "job": {
            "jobId": "sample_job_01",
            "targets": [ "aaa", "bbb", "ccc" ]
        }
    },
    {
        "job": {
            "jobId": "sample_job_02"
        }
    },
    {
        "job": {
            "jobId": "sample_job_03",
            "targets": []
        }
    }
]

まずは先ほどのjqをそのまま実行してみます。

$ cat test.json | jq -r '.[] | .job | .targets |= join(",") | [.jobId, .targets] | @tsv'
sample_job_01   aaa,bbb,ccc
jq: error (at <stdin>:19): Cannot iterate over null (null)

出力を見てみると、最初の1行は出力されているのですが、 2行目で失敗して出力が止まっています。 スキップされるわけではなく中止されてしまうので3行目の出力もありません。 エラーの内容としては「nullは配列的に扱えないよ」という感じです。

では.targetsがnullのものにも適用できるようにしてみます。

$ cat test.json | jq -r '.[] | .job | if .targets then .targets |= join(",") else .targets = "" end | [.jobId, .targets] | @tsv'
sample_job_01   aaa,bbb,ccc
sample_job_02
sample_job_03

ifが出てきました!もうjqでなんでもできるな...。 ということで、ifで出力する内容を分岐できます。 ifの使い方は

if 条件式 then 命令 else 命令 end

です。 ここで命令と書いた部分には、jqにおいて||の間に挟める文言を書けるようです。

if .targets then の部分で.targetsがnullでないかを確認して、 null出なければ配列をjoinし、nullだった場合は""で空文字列を出力するようにしています。 これで目的のTSVが出力されました!

まとめ

jqを使って、Valueとして配列を含み、かつnullもあり得るJSONから@tsvでTSVとして出力するところまでできました!

改行がないとわかりにくいので、最後に改行を入れたコードを貼り付けておきます。 やり方がわかった上でこのコードを見ると特に違和感はないですね。

jq -r '.[]
    | .job
    | if .targets then .targets |= join(",") else .targets = "" end
    | [.jobId, .targets]
    | @tsv'

joinで単純な配列の文字列化と、ifで出力の場合わけができることがわかったので、 JSONから目的の形を取得したい時には広範囲に役立ちそうです!

参考リンク

軽量JSONパーサー『jq』のドキュメント:『jq Manual』をざっくり日本語訳してみました

jq Manual (development version)