知識庫 / JSON RSS 訂閱

JSONPath 簡介

JSON
HongKong
4
09:56 PM · Dec 05 ,2025

1. 概述

XML 的優勢之一在於其處理能力的可獲得性——包括 XPath,後者被定義為 W3C 標準。對於 JSON,出現了一種類似的工具,稱為 JSONPath。

本教程將介紹 Jayway JsonPath,這是一種 Java 實現的 JSONPath 規範。它描述了設置、語法、常用 API 以及使用案例的演示。

2. 安裝配置

要使用 JsonPath,只需在 Maven pom 中添加一個依賴項即可:

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.9.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 路徑。 符號表示有兩種樣式:點和方括號。

以下兩個路徑都指向 JSON 文檔中相同的節點,即在 location 字段中的第三個元素,該元素是 creator 節點的一個子節點,該子節點屬於根節點下的 tool 對象中的 jsonpath 對象。

首先,我們來看使用點符號表示的路徑:

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

現在讓我們來看一下花括號表示法:

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

美元符號($)表示根成員對象。

3.2. 運算符

我們有幾個有用的運算符用於 JsonPath:

  • 根節點 ($) 表示 JSON 結構的根成員,無論它是對象還是數組。我們在上一節中包含了用法示例。
  • 當前節點 (@) 表示正在處理的節點。我們通常將其作為輸入表達式中的一部分,用於謂詞。例如,如果我們在上述 JSON 文檔中處理 數組,表達式 book[?(@.price == 49.99)]指的是該數組中的第一個
  • 通配符 (*) 表示指定範圍內所有元素。例如,book[*]表示book數組內的所有節點。

3.3 函數和過濾器

JsonPath 具有一些函數,我們可以將它們用於路徑末尾以合成路徑的輸出表達式:<em>min()</em>, <em>max()</em>, <em>avg()</em>, `stddev() 和 length()

最後,我們有過濾器。這些是布爾表達式,用於限制返回的節點列表,僅限於調用方法所需的節點。

以下是一些示例:相等性 (<em>==</em>)、正則表達式匹配 (<em>=~</em>)、包含 (<em>in</em>) 和檢查空值 (<em>empty</em>)。我們主要使用過濾器進行謂詞。

對於完整的列表以及不同操作符、函數和過濾器的詳細解釋,請參閲 JsonPath GitHub 項目。

4. 運維

在開始討論運維之前,先做個簡短的説明:本部分使用了我們之前定義的 JSON 示例結構。

4.1. 訪問文檔

JsonPath 提供了一種便捷的方式來訪問 JSON 文檔。我們通過靜態的 read API 來實現這一點:

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

讀取 API 可以與靜態流式 API 配合使用,從而提供更大的靈活性:

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

我們可以使用 read 的其他重載變體,以處理不同類型的 JSON 來源,包括 ObjectInputStreamURLFile

為了簡化問題,本部分測試中未包含參數列表中指定的謂詞(空 varargs)。但我們在後續子章節中將討論 predicates

我們先定義兩個示例路徑進行測試:

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 返回一個 String,其中包含 JsonPath 創建器的名稱,而第二個 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
    }
}

謂詞確定了真假輸入值,用於縮小返回列表,只包含匹配的對象或數組。我們可以輕鬆地將 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);

所有三個 謂詞 示例均已通過以下斷言輔助方法驗證:

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. SPIs

JsonPath 的默認配置,藉助 Option 選項,通常足以滿足大部分任務的需求。然而,對於具有更復雜用例的用户,他們可以根據特定要求修改 JsonPath 的行為——通過三種不同的 SPIs 實現:

  • 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
    }
]

其中,發佈日期 字段的值是自紀元(Epoch)以來的毫秒數,票房 是電影在美國影院的美元收入。

我們將處理五個不同的 GET 請求工作場景,假設上述 JSON 層次結構已提取並存儲在一個名為 jsonStringString 變量中。

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主演 數組中的 標題

以下測試將演示如何執行此操作並驗證返回的結果:

@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. 總收入計算

本場景利用 JsonPath 函數 length() 來確定電影記錄的數量,從而計算所有電影的總收入。

讓我們來看一下實現和測試:

@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 + ")]");

根據計算出的路徑,我們將確定並返回對應電影的 標題:

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>> samMendesMovies = context.read("$[?(@.director == 'Sam Mendes')]");

我們還可以為根節點 director 定義一個 Filter ,並將其用作謂詞:

Filter directorSamMendesFilter = Filter.filter(Criteria.where("director")
    .contains("Sam Mendes"));
List<Map<String, Object>> samMendesMovies = JsonPath.parse(jsonString)
    .read("$[?]", directorSamMendesFilter);

我們隨後使用該列表來提取發佈日期。這些日期將被存儲在一個數組中,然後進行排序:

List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : samMendesMovies) {
    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 存在一些缺點,例如缺乏用於訪問父節點或兄弟節點的操作符,但在許多場景下仍然非常有用。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.