Help us understand the problem. What is going on with this article?

JSONPath 使い方まとめ

目的

先輩「MockMvcでJSONPathを使おうと思ってて…」

@takkii1010 「…?」

となったので、以下を簡単にまとめたい。

  1. JSONPathとは何者か
  2. JSONPathの構文
  3. MockMvcでの使い方

1. JSONPathとは何者か

https://goessner.net/articles/JsonPath/

Data may be interactively found and extracted out of JSON structures on the client without special scripting.
JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document.

  • XPath for JSON
    • 特別なスクリプトを書かずとも、JSONを双方向に検出&抽出できるライブラリ
    • XPathがXMLを参照するのと同様に、JSONPathは常にJSONを参照するようになっている
  • 簡単にいうと、JSONから欲しいkey,value,lengthなどを取得できて便利
  • 意外と身近なところで使われている

2. JSONPathの構文

Jayway JsonPath を見ながら進めます。
また、以下サイトで、JSONPathのお試しがブラウザから簡単にできます。

JSONPath Online Evaluator

基本構文

使ったJSONはこちら。

{
  "firstName": "John",
  "lastName" : "doe",
  "age"      : 26,
  "address"  : {
    "streetAddress": "naist street",
    "city"         : "Nara",
    "postalCode"   : "630-0192"
  },
  "phoneNumbers": [
    {
      "type"  : "iPhone",
      "number": "0123-4567-8888"
    },
    {
      "type"  : "home",
      "number": "0123-4567-8910"
    }
  ]
}
JSONPath 説明 結果 メモ
$ ルートの要素 $.firstName "John" $ だけだと全部取得
@ フィルター結果の現在の要素 $.phoneNumbers[?(@.type == 'home')] "type": "home",
"number": "0123-4567-8910"
* ワイルドカード $.address.* "naist street",
"Nara",
"630-0192"
.. keyが一致するすべてのvalue $..type "iPhone",
"home"
['<name>' (, '<name>')] ブラケット演算子 $.['firstName'] "John"
[<number> (, <number>)] $.phoneNumbers[0,1] {
 "type": "iPhone",
 "number": "0123-4567-8888"
},
{
 "type": "home",
 "number": "0123-4567-8910"
}
[start:end] 配列のスライス $.phoneNumbers[0:1] {
 "type": "iPhone",
 "number": "0123-4567-8888"
}
[?(<expression>)] フィルター機能。式はBooleanで評価されないといけない @ に例記載。

…結果がきれいに書けてない。

関数

min(), max(), avg(), stddev(), length(), sum() があります。
length()$.phoneNumbers.length とあまり深く考えずに使えますが、他の関数が少し厄介。

こんな感じのJSONの場合…

{
  "id": 1,
  "name" : "スクエ…",
  "products": [
    {
      "name"  : "FINAL FANTASY VII",
      "price": 1572
    },
    {
      "name"  : "FINAL FANTASY X",
      "price": 8800
    },
    {
      "name"  : "FINAL FANTASY XV",
      "price": 8800
    }
  ]
}

一度取り出してから、処理を行う必要があるらしい。
https://oboe2uran.hatenablog.com/entry/2018/05/06/131634

MockMvcを使う場合は以下で動きました。

        val jsonObject = mockMvc.perform(get("/gameCompanies/1"))
                .andExpect(status().isOk())
                .andReturn().response.contentAsString
        val priceList: List<Int> = JsonPath.read( jsonObject, "\$.products[*].price")
        val min: Int = JsonPath.read(priceList, "\$.min()")
        val max: Int = JsonPath.read(priceList, "\$.max()")
        val sum: Int = JsonPath.read(priceList, "\$.sum()")
        val stddev: Int = JsonPath.read(priceList, "\$.stddev()")
        assertThat(min).isEqualTo(1572)
        assertThat(max).isEqualTo(8800)
        assertThat(sum).isEqualTo(19172)
        assertThat(stddev).isEqualTo(3407)

他にこちらにも例が載ってます

3. MockMvcでの使い方

@RunWith(SpringRunner::class)
@WebMvcTest(GameCompanyController::class)
class GameCompanyControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

    @MockBean
    lateinit var gameCompanyRepository: GameCompanyRepositoryInterface

    @Test
    fun getGameCompanyTest() {
        val expectedGameCompany = GameCompany(
                id = 1L,
                name = "スクエ…",
                products = listOf(
                        Product("FINAL FANTASY VII", BigDecimal(1572)),
                        Product("FINAL FANTASY X", BigDecimal(8800)),
                        Product("FINAL FANTASY XV", BigDecimal(8800))
                )
        )

        `when`(gameCompanyRepository.fetchGameCompany(any())).thenReturn(expectedGameCompany)
        mockMvc.perform(get("/gameCompanies/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("\$.id").value(1))
                .andExpect(jsonPath("\$.name").value("スクエ…"))
                .andExpect(jsonPath("\$.products[0].name").value("FINAL FANTASY VII"))
                .andExpect(jsonPath("\$.products[0].price").value(1572))
                .andExpect(jsonPath("\$.products[1].name").value("FINAL FANTASY X"))
                .andExpect(jsonPath("\$.products[1].price").value(8800))
                .andExpect(jsonPath("\$.products[2].name").value("FINAL FANTASY XV"))
                .andExpect(jsonPath("\$.products[2].price").value(8800))
    }
}

Kotlinの indices 使うとこんな感じ。

        
        mockMvc.perform(get("/gameCompanies/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                ...
                .andExpect(expectedGameCompany.products)
    }

    private fun ResultActions.andExpect(products: List<Product>): ResultActions {
        for (index in products.indices) {
            andExpect(jsonPath("\$.products[$index].name").value(products[index].name))
                    .andExpect(jsonPath("\$.products[$index].price").value(products[index].price))
        }
        return this
    }

他にも、 Matchers のisを使用しても動きます。

        mockMvc.perform(get("/gameCompanies/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("\$.id", `is`(1)))

ただ、少し古い書き方のようなので、 value で書くのが良さそう。

参考にしたサイト一覧

https://goessner.net/articles/JsonPath/
https://www.techscore.com/tech/XML/XPath/XPath1/xpath01.html/
https://github.com/json-path/JsonPath
http://oboe2uran.hatenablog.com/entry/2018/05/06/131634

takkii1010
コード書き始めて2年目。 ここでの発言はあくまでも個人の見解であり、所属企業を代表するものではありませんし、わたしの誕生日は10月10日でもありません。
tis
創業40年超のSIerです。
https://www.tis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした