JSONにpathでアクセスするには? | JavaScript | プログラミング

JSONデータの各部にアクセスするために、ごく簡単なJavaScript関数を書いてみた。この小さなプログラムは、ツリー構造のパス式の一種を実装していると考えられる。一般的なパス式(path expression)の説明をしてから、今回定義したJSON向けの簡単なパス式を紹介しよう。

記事のバナー

ファイルシステムのパス

多くの人が「パス」という言葉から最初に連想するモノは、おそらくファイルシステムのパスだろう。例えば、Linuxなら、/home/hiyama/.bashrc がファイルのパスとなる。これは、前回の記事「一人で使うGit」で話題にしたbashの設定ファイルのパスだ。同様なファイルのパスがWindowsなら、C:\Users\hiyama\.bashrc となる。

これらのパスは、ファイルシステムのツリー構造と関連している。ファイルシステム・ツリー内における /home/hiyama/.bashrc は、次のように図示できる。

ファイルシステムのツリー

パスは、区切り記号(デリミータ、セパレータ)でいくつかの部分に分離できる。Linuxパスの区切り記号はスラッシュ「/」であり、/home/hiyama/.bashrc ならば、「home」、「hiyama」、「.barshr」という部分に分離できる。区切り記号で分離されるパスの各部分をセグメントと呼ぶ。

Windowsの場合は、区切り記号がバックスラッシュ「\」となる。環境によりバックスラッシュは円マークに見えるかもしれない。Windowsのパス C:\Users\hiyama\.bashrc は、「C:」、「Users」、「hiyama」、「.bashrc」というセグメントに分離される。ドライブ 「C:」があるのがWindows特有だが、単一ドライブ内のファイルシステムとパスの構造は、WindowsもLinuxと似たようなものだ。

その他のパス式の例

インターネットのドメイン名もパス式の例となる。例えば、www.symmetric.co.jp というドメイン名では、区切り記号がドット(ピリオド)で、「www」、「symmetric」、「co」、「jp」という4つのセグメントからなる。しかし、階層の順序がファイルのパスとは逆順になっている。

階層の上位から下位に向かう順序なら、jp.co.symmetric.www となる。Javaのパッケージ名では、この順序(ドメイン名とは逆順)が使われている。シンメトリック社のユーティリティ・パッケージなら jp.co.symmetric.util のような命名になる。このパッケージ名も、もちろんパス式の例である。

JavaScriptをはじめ多くのプログラミング言語では、オブジェクトのプロパティやメソッドへのアクセスに、ドットを区切り記号とするパス式風の構文が使われる。例えば、次のJSONデータ(JavaScriptのデータでもある)を考えてみよう。

var aPerson = {
  "name": "坂東トン吉",
  "email":{
    "biz": "bandou@example.co.jp",
    "private": "tonkichi@mail.example.org"
  }
};

この人物データの名前(nameプロパティ)の部分にアクセスするには、aPerson.name となる。個人のemailアドレスなら、aPerson.email.private によってアクセスできる。このパス式では、区切り記号はドットで、セグメントは階層の上位から下位に向かう順序で並んでいる。

JSONデータの各部にアクセスするパス式

ここから先はJSONデータについて考えよう。例として次のJSONデータを考える(先ほどとは別なデータ)。

{
  "greeting": ["hello", "world"], 
  "polite": true,
  "position": {"x":20, "y":10}
}

このデータを、一種のツリー構造として図示することができる。例えば以下のようだ。

JSONデータのツリー

グレーのマルは複合データ(オブジェクトまたは配列)を表している。緑の角丸四角は基本データ(スカラー)、つまり数値、文字列、ブール値などだ。

この図は、データ構造をうまく表現しているが、各ノードにパスを割り当ててはいない。各ノードのデータ内での位置を示すパス式を考えてみよう。

まずは、ファイルシステムと同様な記法を使ってみる。オブジェクトと配列をディレクトリ(フォルダー)のように考えて、末端の数値/文字列/ブール値などはファイルのようにみなそう。すると、各部のパスは次のようになる。

番号 パス
1 / {“greeting”: [“hello”, “world”], “polite”: true, “position”: {“x”:20, “y”:10}}
2 /greeting [“hello”, “world”]
3 /greeting/0 “hello”
4 /greeting/1 “world”
5 /polite true
6 /position {“x”:20, “y”:10}
7 /position/x 20
8 /position/y 10

プロパティ名である「greeting」や「polite」と、配列のインデックス(項目番号)である「0」、「1」が同じ記法なのが気になるかもしれない。だけど、JavaScriptだと、オブジェクト {“0”: “hello”, “1”:”world”} と配列 [“hello”, “world”] は同じような扱いとなる。

> ({"0": "hello", "1":"world"})[1]
  "world"
> (["hello", "world"])["1"] 
  "world"
>

JSON仕様は、JavaScriptのデータの扱いに完全に従っているわけではない。JSONではオブジェクトと配列がキチンと区別されているのだが、次の約束事をすれば、プロパティ名とインデックス(配列の項目番号)を一律に扱っても差支えはない。

  • オブジェクトのプロパティ名に数字だけの名前、ドット・空白・引用符を含む名前を使わない。

それより、区切り記号がスラッシュであるほうが違和感があるのではないだろうか。スラッシュをドットに変更すると、パスは次のようになる。

  1. .
  2. .greeting
  3. .greeting.0
  4. .greeting.1
  5. .polite
  6. .position
  7. .position.x
  8. .position.y

ルート(データ全体)を表す先頭のピリオドはあらずもがななので省略すると:

  1. .
  2. greeting
  3. greeting.0
  4. greeting.1
  5. polite
  6. position
  7. position.x
  8. position.y

となる。この単純な方式でも、与えられたJSONデータの任意の部分にアクセスすることができる。

JavaScriptによる実装

上で述べたような簡単なパス式なら、JavaScriptの関数ひとつで実装できる。これでもけっこう実用になったりする。

function getSubdata(data, path) {
  var val = data;
  var segments = path.split('.');
  for (var i = 0; i < segments.length && val !== undefined; i++) {
    var seg = segments[i];
    if (seg === "") {
      // セグメントが空ならば、何もしない
      ; 
    } else {
      if (typeof val === 'object') {
        // オブジェクトデータ(配列も含まれる)のときは
        // オブジェクト・プロパティ、または配列項目を取る
        val = val[seg];
      } else {
        // オブジェクト以外のデータではプロパティ/項目は考えない
        val = undefined; 
      }
    }
  }
  return val;
}

以下は、Chromeのコンソールによる実行例である。

> var data = {"greeting": ["hello", "world"], "polite": true, "position": {"x":20, "y":10}}
  undefined
> getSubdata(data, ".")
  ▶ Object {greeting: Array[2], polite: true, position: Object}
> getSubdata(data, "greeting")
  ["hello", "world"]
> getSubdata(data, "greeting.0")
  "hello"
> getSubdata(data, "greeting.1")
  "world"
> getSubdata(data, "polite")
  true
> getSubdata(data, "position")
  Object {x: 20, y: 10}
> getSubdata(data, "position.x")
  20
> getSubdata(data, "position.y")
  10
> 

細かいことを言えば、次のような点を明確化する必要がある。

  • greeting..0 のような、引き続く2個のピリオドを許すか。(現状は許している)
  • polite. のような、最後のピリオドを許すか。(現状は許している)
  • 文字列””で表現される空なパスを許すか。(現状は許している)

XMLの世界では、XMLデータの一部を取り出すためのXPathという複雑なパス式があるのだが、簡素なデータ形式であるJSONでは、今述べた程度の単純なパス式でも間に合う場面が多い。JSONに対してもパス式を活用してみよう。必要があればより厳密化したり拡張したりすればいいのだ。


JSONはJavaScriptの構文を採用しているが、実はJavaScriptのようにゆるくなく、けっこう厳しい構文規則がある。その点に関しては、私の個人ブログ記事「もう一度、ちゃんとJSON入門」を参照していただきたい。

JSONに対してもXPathのような高機能なパス式を実現する試みは古くからある。JSONPathがそのようなものだ。