1. 概要

XMLの利点の1つは、W3C標準として定義されているXPathを含む処理の可用性です。 JSONの場合、JSONPathと呼ばれる同様のツールが登場しました。

このチュートリアルでは、JSONPath仕様のJava実装であるJaywayJsonPath紹介を行います。 セットアップ、構文、一般的なAPI、およびユースケースのデモンストレーションについて説明します。

2. 設定

JsonPathを使用するには、Mavenpomに依存関係を含める必要があります。

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
</dependency>

3. 構文

次のJSON構造を使用して、JsonPathの構文とAPIを示します。

{
    "tool": 
    {
        "jsonpath": 
        {
            "creator": 
            {
                "name": "Jayway Inc.",
                "location": 
                [
                    "Malmo",
                    "San Francisco",
                    "Helsingborg"
                ]
            }
        }
    },

    "book": 
    [
        {
            "title": "Beginning JSON",
            "price": 49.99
        },

        {
            "title": "JSON at Work",
            "price": 29.99
        }
    ]
}

3.1. 表記

JsonPathは、特別な表記法を使用して、ノードと、JsonPathパス内の隣接ノードへの接続を表します。 表記には、ドットとブラケットの2つのスタイルがあります。

次のパスは両方とも、上記のJSONドキュメントの同じノードを参照しています。これは、creatorノードのlocation フィールド内の3番目の要素であり、の子です。ルートノードの下のtoolに属するjsonpathオブジェクト。

まず、ドット表記のパスが表示されます。

$.tool.jsonpath.creator.location[2]

次に、角かっこ表記を見てみましょう。

$['tool']['jsonpath']['creator']['location'][2]

ドル記号($)は、ルートメンバーオブジェクトを表します。

3.2. オペレーター

JsonPathにはいくつかの便利な演算子があります。

  • ルートノード($)は、JSON構造がオブジェクトであるか配列であるかに関係なく、JSON構造のルートメンバーを示します。 前のサブセクションに使用例を含めました。
  • 現在のノード(@)は、処理中のノードを表します。 主に、述語の入力式の一部として使用します。 上記のJSONドキュメントでbook配列を処理しているとします。 式book[?(@。price == 49.99)] は、その配列の最初のbookを指します。
  • ワイルドカード(*)は、指定されたスコープ内のすべての要素を表します。 たとえば、 book [*] は、bookアレイ内のすべてのノードを示します。

3.3. 関数とフィルター

JsonPathには、パスの最後でそのパスの出力式を合成するために使用できる関数もあります: min() max() avg() stddev()および length()

最後に、フィルターがあります。 これらは、返されるノードのリストを呼び出しメソッドが必要とするものだけに制限するブール式です。

いくつかの例は、等式( == )、正規表現マッチング( =〜)、包含( in )、および空のチェック( empty[ X144X])。 主に述語にフィルターを使用します。

さまざまな演算子、関数、フィルターの完全なリストと詳細な説明については、 JsonPathGitHubプロジェクトを参照してください。

4. オペレーション

操作に入る前に、簡単な補足:このセクションでは、前に定義したJSONのサンプル構造を使用します。

4.1. ドキュメントへのアクセス

JsonPathには、JSONドキュメントにアクセスするための便利な方法があります。 これは、静的 readAPIを介して行います。

<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);

read APIは、静的な流暢なAPIと連携して、柔軟性を高めることができます。

<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);

Object InputStream URL Fileなど、さまざまなタイプのJSONソースにreadの他のオーバーロードされたバリアントを使用できます

簡単にするために、この部分のテストでは、パラメーターリストに述語が含まれていません(空の varargs )。 ただし、述語については後のサブセクションで説明します。

作業する2つのサンプルパスを定義することから始めましょう。

String jsonpathCreatorNamePath = "$['tool']['jsonpath']['creator']['name']";
String jsonpathCreatorLocationPath = "$['tool']['jsonpath']['creator']['location'][*]";

次に、指定されたJSONソース jsonDataSourceString を解析して、DocumentContextオブジェクトを作成します。 新しく作成されたオブジェクトは、上記で定義されたパスを使用してコンテンツを読み取るために使用されます。

DocumentContext jsonContext = JsonPath.parse(jsonDataSourceString);
String jsonpathCreatorName = jsonContext.read(jsonpathCreatorNamePath);
List<String> jsonpathCreatorLocation = jsonContext.read(jsonpathCreatorLocationPath);

最初のread APIは、JsonPath作成者の名前を含む String を返し、2番目のAPIはそのアドレスのリストを返します。

そして、JUnit Assert APIを使用して、メソッドが期待どおりに機能することを確認します。

assertEquals("Jayway Inc.", jsonpathCreatorName);
assertThat(jsonpathCreatorLocation.toString(), containsString("Malmo"));
assertThat(jsonpathCreatorLocation.toString(), containsString("San Francisco"));
assertThat(jsonpathCreatorLocation.toString(), containsString("Helsingborg"));

4.2. 述語

基本がわかったので、作業する新しいJSONの例を定義し、述語を作成して使用する方法を説明しましょう。

{
    "book": 
    [
        {
            "title": "Beginning JSON",
            "author": "Ben Smith",
            "price": 49.99
        },

        {
            "title": "JSON at Work",
            "author": "Tom Marrs",
            "price": 29.99
        },

        {
            "title": "Learn JSON in a DAY",
            "author": "Acodemy",
            "price": 8.99
        },

        {
            "title": "JSON: Questions and Answers",
            "author": "George Duckett",
            "price": 6.00
        }
    ],

    "price range": 
    {
        "cheap": 10.00,
        "medium": 20.00
    }
}

述語は、フィルターのtrueまたはfalseの入力値を判別して、返されたリストを一致するオブジェクトまたは配列のみに絞り込みます。 Predicateを静的ファクトリメソッドの引数として使用することでFilterに簡単に統合できます。 要求されたコンテンツは、そのFilterを使用してJSON文字列から読み取ることができます。

Filter expensiveFilter = Filter.filter(Criteria.where("price").gt(20.00));
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
  .read("$['book'][?]", expensiveFilter);
predicateUsageAssertionHelper(expensive);

カスタマイズしたPredicateを定義し、それを readAPIの引数として使用することもできます。

Predicate expensivePredicate = new Predicate() {
    public boolean apply(PredicateContext context) {
        String value = context.item(Map.class).get("price").toString();
        return Float.valueOf(value) > 20.00;
    }
};
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
  .read("$['book'][?]", expensivePredicate);
predicateUsageAssertionHelper(expensive);

最後に、述語は、オブジェクトを作成せずに read APIに直接適用できます。これは、インライン述語と呼ばれます。

List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
  .read("$['book'][?(@['price'] > $['price range']['medium'])]");
predicateUsageAssertionHelper(expensive);

上記の述語の3つの例はすべて、次のアサーションヘルパーメソッドを使用して検証されます。

private void predicateUsageAssertionHelper(List<?> predicate) {
    assertThat(predicate.toString(), containsString("Beginning JSON"));
    assertThat(predicate.toString(), containsString("JSON at Work"));
    assertThat(predicate.toString(), not(containsString("Learn JSON in a DAY")));
    assertThat(predicate.toString(), not(containsString("JSON: Questions and Answers")));
}

5. 構成

5.1. オプション

Jayway JsonPathには、デフォルト構成を微調整するためのいくつかのオプションがあります。

  • Option.AS_PATH_LIST は、値の代わりに評価ヒットのパスを返します。
  • Option.DEFAULT_PATH_LEAF_TO_NULL は、欠落しているリーフに対してnullを返します。
  • Option.ALWAYS_RETURN_LIST は、パスが明確な場合でもリストを返します。
  • Option.SUPPRESS_EXCEPTIONS は、パス評価から例外が伝播されないようにします。
  • Option.REQUIRE_PROPERTIES では、不定パスが評価されるときにパスで定義されたプロパティが必要です。

オプションを最初から適用する方法は次のとおりです。

Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();

およびそれを既存の構成に追加する方法:

Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);

5.2. SPI

Option を使用したJsonPathのデフォルト構成は、ほとんどのタスクに十分なはずです。 ただし、より複雑なユースケースを持つユーザーは、3つの異なるSPIを使用して、特定の要件に従ってJsonPathの動作を変更できます。

  • JsonProvider SPIを使用すると、JsonPathがJSONドキュメントを解析および処理する方法を変更できます。
  • MappingProvider SPIを使用すると、ノード値と返されたオブジェクトタイプ間のバインディングをカスタマイズできます。
  • CacheProvider SPIは、パスがキャッシュされる方法を調整します。これにより、パフォーマンスが向上します。

6. ユースケースの例

これで、JsonPathの機能について十分に理解できました。 それでは、例を見てみましょう。

このセクションでは、Webサービスから返されたJSONデータの処理について説明します。

次の構造を返す映画情報サービスがあるとします。

[
    {
        "id": 1,
        "title": "Casino Royale",
        "director": "Martin Campbell",
        "starring": 
        [
            "Daniel Craig",
            "Eva Green"
        ],
        "desc": "Twenty-first James Bond movie",
        "release date": 1163466000000,
        "box office": 594275385
    },

    {
        "id": 2,
        "title": "Quantum of Solace",
        "director": "Marc Forster",
        "starring": 
        [
            "Daniel Craig",
            "Olga Kurylenko"
        ],
        "desc": "Twenty-second James Bond movie",
        "release date": 1225242000000,
        "box office": 591692078
    },

    {
        "id": 3,
        "title": "Skyfall",
        "director": "Sam Mendes",
        "starring": 
        [
            "Daniel Craig",
            "Naomie Harris"
        ],
        "desc": "Twenty-third James Bond movie",
        "release date": 1350954000000,
        "box office": 1110526981
    },

    {
        "id": 4,
        "title": "Spectre",
        "director": "Sam Mendes",
        "starring": 
        [
            "Daniel Craig",
            "Lea Seydoux"
        ],
        "desc": "Twenty-fourth James Bond movie",
        "release date": 1445821200000,
        "box office": 879376275
    }
]

ここで、リリース日フィールドの値はエポックからのミリ秒であり、興行収入は映画館での映画の米ドルでの収益です。

上記のJSON階層が抽出され、jsonStringという名前のString変数に格納されていると仮定して、GETリクエストに関連する5つの異なる作業シナリオを処理します。

6.1. IDを指定してオブジェクトデータを取得する

このユースケースでは、クライアントはサーバーに映画の正確な id を提供することにより、特定の映画に関する詳細情報を要求します。 この例は、サーバーがクライアントに戻る前に要求されたデータを探す方法を示しています。

idが2に等しいレコードを見つける必要があるとします。

最初のステップは、正しいデータオブジェクトを取得することです。

Object dataObject = JsonPath.parse(jsonString).read("$[?(@.id == 2)]");
String dataString = dataObject.toString();

JUnit Assert APIは、いくつかのフィールドの存在を確認します。

assertThat(dataString, containsString("2"));
assertThat(dataString, containsString("Quantum of Solace"));
assertThat(dataString, containsString("Twenty-second James Bond movie"));

6.2. 主演を与えられた映画のタイトルを取得する

EvaGreenという女優が主演する映画を探したいとしましょう。 サーバーは、starring配列にEvaGreenを含む映画のtitleを返す必要があります。

後続のテストは、それを実行し、返された結果を検証する方法を示します。

@Test
public void givenStarring_whenRequestingMovieTitle_thenSucceed() {
    List<Map<String, Object>> dataList = JsonPath.parse(jsonString)
      .read("$[?('Eva Green' in @['starring'])]");
    String title = (String) dataList.get(0).get("title");

    assertEquals("Casino Royale", title);
}

6.3. 総収入の計算

このシナリオでは、 length()と呼ばれるJsonPath関数を使用して、すべての映画の総収益を計算するために映画レコードの数を計算します。

実装とテストを見てみましょう。

@Test
public void givenCompleteStructure_whenCalculatingTotalRevenue_thenSucceed() {
    DocumentContext context = JsonPath.parse(jsonString);
    int length = context.read("$.length()");
    long revenue = 0;
    for (int i = 0; i < length; i++) {
        revenue += context.read("$[" + i + "]['box office']", Long.class);
    }

    assertEquals(594275385L + 591692078L + 1110526981L + 879376275L, revenue);
}

6.4. 最高の収入の映画

このユースケースは、デフォルト以外のJsonPath構成オプション、つまり Option.AS_PATH_LIST を使用して、収益が最も高い映画を見つける例です。

まず、すべての映画の興行収入のリストを抽出する必要があります。 次に、それを並べ替え用の配列に変換します。

DocumentContext context = JsonPath.parse(jsonString);
List<Object> revenueList = context.read("$[*]['box office']");
Integer[] revenueArray = revenueList.toArray(new Integer[0]);
Arrays.sort(revenueArray);

revenueArrayで並べ替えられた配列からhighestRevenue変数を簡単に取得し、それを使用して、収益が最も高い映画レコードへのパスを計算できます。

int highestRevenue = revenueArray[revenueArray.length - 1];
Configuration pathConfiguration = 
  Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(pathConfiguration).parse(jsonString)
  .read("$[?(@['box office'] == " + highestRevenue + ")]");

その計算されたパスに基づいて、対応する映画のtitleを決定して返します。

Map<String, String> dataRecord = context.read(pathList.get(0));
String title = dataRecord.get("title");

プロセス全体は、 AssertAPIによって検証されます。

assertEquals("Skyfall", title);

6.5. 監督の最新映画

この例では、 SamMendesという名前の監督が監督した最後の映画を理解する方法を説明します。

まず、 SamMendesが監督したすべての映画のリストを作成します。

DocumentContext context = JsonPath.parse(jsonString);
List<Map<String, Object>> dataList = context.read("$[?(@.director == 'Sam Mendes')]");

次に、そのリストを使用してリリース日を抽出します。 これらの日付は配列に格納されてから並べ替えられます。

List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : dataList) {
    Object date = item.get("release date");
    dateList.add(date);
}
Long[] dateArray = dateList.toArray(new Long[0]);
Arrays.sort(dateArray);

lastestTime 変数(ソートされた配列の最後の要素)を director フィールドの値と組み合わせて使用して、要求された映画のtitleを決定します。

long latestTime = dateArray[dateArray.length - 1];
List<Map<String, Object>> finalDataList = context.read("$[?(@['director'] 
  == 'Sam Mendes' && @['release date'] == " + latestTime + ")]");
String title = (String) finalDataList.get(0).get("title");

次のアサーションは、すべてが期待どおりに機能することを証明します。

assertEquals("Spectre", title);

7. 結論

この記事では、JSONドキュメントをトラバースおよび解析するための強力なツールであるJaywayJsonPathの基本的な機能について説明しました。

JsonPathには、親ノードまたは兄弟ノードに到達するための演算子がないなど、いくつかの欠点がありますが、多くのシナリオで非常に役立ちます。

これらすべての例とコードスニペットの実装は、GitHubにあります。