24
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

JSONPath 使い方まとめ

目的

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

@takkii1010 「…?」

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

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

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

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
24
Help us understand the problem. What are the problem?