JSONSassertで正規表現によるマッチング処理を適用する

JavaでJSONを比較するライブラリとして[JSONassert](http://jsonassert.skyscreamer.org/)というものがあるのですが、今回はそのライブラリでJSONを比較する際に、値のマッチングに正規表現を適用する方法について簡単に紹介したいと思います。

# 検証バージョン

* JSONassert 1.5.0

# 標準でサポートされている方法を利用する

以下のように期待値を正規表現として扱いたい項目を`CustomComparator`を利用して指定します。

```json:検証対象のJSON
{
  "array": [
    1,
    2
  ],
  "id": 100,
  "type": "foo",
  "object": {
    "name": "Kazuki",
    "token": "ac648657-797e-49f7-9563-150c9b5c2284"
  }
}
```

```json:期待値を表現するJSON
{
  "array": [
    1,
    2
  ],
  "id": 100,
  "type": "foo",
  "object": {
    "name": "Kazuki",
    "token": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
  }
}
```

以下は、`object.token` を正規表現を使って比較する際の指定例になります。

```java:比較メソッドの呼び出し例
JSONAssert.assertEquals(expectedJson.toString(2), actualJson.toString(2),
    new CustomComparator(JSONCompareMode.STRICT,
        new Customization("object.token", new RegularExpressionValueMatcher<>()) // 期待値に指定したパターンを使ってマッチングする
    )
);
```

また、以下のように検証時にパターンを指定することもできます。

```java:検証時にパターンを指定する例
JSONAssert.assertEquals(expectedJson.toString(2), actualJson.toString(2),
    new CustomComparator(JSONCompareMode.STRICT,
        new Customization("object.token", new RegularExpressionValueMatcher<>("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")) // 期待値のJSONに記載されている値は利用せず、ここで指定したパターンを使ってマッチングする
    )
);
```

# 全ての項目を正規表現によるマッチング対象にする

標準機能では、（私が把握している範囲では）正規表現の適用は項目単位になります（ひょっとしたらパスにワイルドカード的な指定方法がサポートされているかも！？）。なので・・・ここでは`CustomComparator`は使わずに、全ての項目を正規表現を使ってマッチングすることができる独自の`JSONComparator`を作成したいと思います。


```java:JSONComparatorの作成例
public class RegularExpressionComparator extends DefaultComparator {

  private static final RegularExpressionValueMatcher<Object> REGEX_VALUE_MATCHER = new RegularExpressionValueMatcher<>();

  private final ValueMatcher<Object> matcher;

  public RegularExpressionComparator(JSONCompareMode mode) {
    super(mode);
    // 一旦完全一致で比較し、NGの場合は正規表現でのマッチングへfallbackする方式を採用
    // ほとんどの項目は完全一致で良いため、無条件で正規表現によるマッチングを行うよりも処理コストを減らすことができる（はず）
    this.matcher = (actual, expected) -> Objects.equals(actual, expected) ||
        expected instanceof String && REGEX_VALUE_MATCHER.equal(actual, expected);
  }

  @Override
  public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException {
    if (actualValue instanceof JSONArray || actualValue instanceof JSONObject) {
      // コンテナ(オブジェクト＋配列)オブジェクトの場合はデフォルト実装へ移譲する
      super.compareValues(prefix, expectedValue, actualValue, result);
    } else {
      // 値（文字列、数値、真偽値）の場合は正規表現サポートのValueMatcherを呼び出して比較する
      try {
        if (!matcher.equal(actualValue, expectedValue)) {
          // 正規表現によるマッチング対象外の時に不一致と判断された場合のエラー処理
          result.fail(prefix, expectedValue, actualValue);
        }
      } catch (ValueMatcherException e) {
        // 正規表現によるマッチングで不一致と判断された場合のエラー処理
        result.fail(prefix, e);
      }
    }
  }

}
```

# 検証用に作成したテストケース

参考までに、検証時に作成したテストケースも貼り付けておきます。

```java
package com.example.assertdemo;

import java.util.Objects;
import java.util.UUID;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.RegularExpressionValueMatcher;
import org.skyscreamer.jsonassert.ValueMatcher;
import org.skyscreamer.jsonassert.ValueMatcherException;
import org.skyscreamer.jsonassert.comparator.DefaultComparator;

public class JSONAssertRegexTests {

  @Test
  public void regexAssert() throws JSONException {

    JSONObject expectedJson = new JSONObject();
    {
      JSONArray array = new JSONArray();
      array.put(1);
      array.put(2);
      JSONObject object = new JSONObject();
      object.put("name", "Kazuki");
      object.put("token", "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
      expectedJson.put("id", 100);
      expectedJson.put("type", "foo");
      expectedJson.put("array", array);
      expectedJson.put("object", object);
      System.out.println("-----expectedJson------");
      System.out.println(expectedJson.toString(2));
    }

    JSONObject actualJson = new JSONObject();
    {
      JSONArray array = new JSONArray();
      array.put(1);
      array.put(2);
      actualJson.put("id", 100);
      actualJson.put("array", array);
      JSONObject object = new JSONObject();
      object.put("name", "Kazuki");
      object.put("token", UUID.randomUUID().toString());
      actualJson.put("id", 100);
      actualJson.put("type", "foo");
      actualJson.put("array", array);
      actualJson.put("object", object);
      System.out.println("-----actualJson------");
      System.out.println(actualJson.toString(2));
    }

    JSONAssert.assertEquals(expectedJson.toString(2),
        actualJson.toString(2),
        new RegularExpressionComparator(JSONCompareMode.STRICT));

  }

  public static class RegularExpressionComparator extends DefaultComparator {

    private static final RegularExpressionValueMatcher<Object> REGEX_VALUE_MATCHER = new RegularExpressionValueMatcher<>();

    private final ValueMatcher<Object> matcher;

    public RegularExpressionComparator(JSONCompareMode mode) {
      super(mode);
      this.matcher = (actual, expected) -> Objects.equals(actual, expected) ||
          expected instanceof String && REGEX_VALUE_MATCHER.equal(actual, expected);
    }

    @Override
    public void compareValues(String prefix, Object expectedValue, Object actualValue, JSONCompareResult result) throws JSONException {
      if (actualValue instanceof JSONArray || actualValue instanceof JSONObject) {
        super.compareValues(prefix, expectedValue, actualValue, result);
      } else {
        try {
          if (!matcher.equal(actualValue, expectedValue)) {
            result.fail(prefix, expectedValue, actualValue);
          }
        } catch (ValueMatcherException e) {
          result.fail(prefix, e);
        }
      }
    }

  }

}
```

# まとめ

標準機能だけでも問題はないのですが、「全ての項目を正規表現によるマッチング対象にする」実装が欲しくなったのは・・・・Web APIのレスポンスBODYの検証を行う際に、期待するJSONをファイルとして用意しておき、実際にレスポンスされたデータと期待値ファイルから読み込んだJSONを比較するような仕組みを採用する上で、個々のテストケースの中で`CustomComparator`を使って対象項目を指定するのが（ちょっと）つらいな〜と思い、期待値ファイルに正規表現を指定すれば自動的に適用できるようにしたかったためです。

# 参考サイト

* http://jsonassert.skyscreamer.org/
* https://www.baeldung.com/jsonassert