JsonPathの概要

1. 概要

XMLの利点の1つは、処理の可用性(XPathを含む)です。これは、https://www.w3.org/TR/xpath/ [W3C標準]として定義されています。 JSONについては、JSONPathと呼ばれる同様のツールが登場しました。
この記事では、http://goessner.net/articles/JsonPath/ [JSONPath specification]のJava実装であるJayway JsonPath *を紹介します。 セットアップ、構文、一般的なAPI、および使用例のデモンストレーションについて説明します。

参考文献:

link:/integration-testing-in-spring [春の統合テスト]

Spring Webアプリケーションの統合テストを作成するためのクイックガイド。
link:/integration-testing-in-spring [続きを読む]↠’

2. セットアップ

JsonPathを使用するには、Maven pomに依存関係を含めるだけです。
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
</dependency>

3. 構文

このセクションでは、JsonPathの構文とAPIを示すために、次のJSON構造を使用します。
{
    "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ドキュメントで_book_配列を扱っているとすると、式_book [?(@。price == 49.99)] _はその配列の最初の_book_を参照します。
  • Wildcard(:指定されたスコープ内のすべての要素を表します。 たとえば、_book [*] _は、_book_配列内のすべてのノードを示します。

* 3.3。 関数とフィルター*

JsonPathには、パスの最後まで使用して、そのパスの出力式を合成できる関数もあります:_min()_、_ max()_、_ avg()_、_ stddev()_、_ length()_。
最後に-フィルタがあります。これらは、返されるノードのリストをメソッドの呼び出しに必要なものだけに制限するブール式です。
いくつかの例は、平等(_ == _)、正規表現一致(_ =〜_)、包含(_in_)、空のチェック(_empty_)です。 フィルタは主に述語に使用されます。
さまざまな演算子、関数、フィルターの完全なリストと詳細な説明については、https://github.com/jayway/JsonPath [JsonPath GitHub]プロジェクトを参照してください。

4. オペレーション

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

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

JsonPathには、静的_read_ APIを介してJSONドキュメントにアクセスする便利な方法があります。
<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);
_read_ APIは、静的な流APIなAPIと連携して、柔軟性を高めます。
<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);
_Object _、_ InputStream _、_ URL _、_ File_など、さまざまなタイプのJSONソースに対して、_read_の他のオーバーロードされたバリアントを使用できます。
物事を簡単にするために、この部分のテストではパラメーターリストに述語を含めません(空の_varargs_)。 _predicates_については、後のサブセクションで説明します。
作業する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番目はそのアドレスのリストを返します。 そして、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
    }
}
述語は、一致するオブジェクトまたは配列のみに返されるリストを絞り込むためのフィルターの真または偽の入力値を決定します。 _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_を定義し、_read_ APIの引数として使用することもできます。
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つの_Predicate_の例はすべて、次のアサーションヘルパーメソッドを使用して検証されます。
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:パスで定義されたプロパティが必要です
    不定パスが評価されるとき。

    _Option_を最初から適用する方法は次のとおりです。
Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();
既存の構成に追加する方法:
Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);

* 5.2。 SPI *

_Option_を使用したJsonPathのデフォルト設定は、ほとんどのタスクに十分なはずです。 ただし、より複雑なユースケースを持つユーザーは、特定の要件に応じてJsonPathの動作を変更できます。3つの異なるSPIを使用します。
  • 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
    }
]
ここで、_release date_フィールドの値は、エポック以降の期間(ミリ秒)であり、_box office_は映画館での映画の収益(米ドル)です。
上記の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。 主演の映画タイトルを取得する*

_Eva Green_という女優が出演する映画を探したいとしましょう。 サーバーは、_Eva Green_が_starring_配列に含まれている映画の_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);
_highestRevenue_変数は、_revenueArray_ソートされた配列から簡単に選択でき、最も高い収入の映画レコードへのパスを計算するために使用できます。
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");
プロセス全体は、_Assert_ APIによって検証されます。
assertEquals("Skyfall", title);

* 6.5。 監督の最新ムービー*

この例では、_Sam Mendes_という監督が監督した長編映画を把握する方法を説明します。
まず、_Sam Mendes_が監督したすべての映画のリストが作成されます。
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. 結論

このチュートリアルでは、Jayway JsonPathの基本機能について説明しました。これは、JSONドキュメントをトラバースおよび解析するための強力なツールです。
JsonPathには、親ノードや兄弟ノードに到達するための演算子がないなど、いくつかの欠点がありますが、多くのシナリオで非常に役立ちます。
これらすべての例とコードスニペットの実装は、* https://github.com/eugenp/tutorials/tree/master/json-path [GitHubプロジェクト] *にあります。