JavaでJSONを比較するライブラリとしてJSONassertというものがあるのですが、今回はそのライブラリでJSONを比較する際に、値のマッチングに正規表現を適用する方法について簡単に紹介したいと思います。
検証バージョン
- JSONassert 1.5.0
標準でサポートされている方法を利用する
以下のように期待値を正規表現として扱いたい項目をCustomComparator
を利用して指定します。
{
"array": [
1,
2
],
"id": 100,
"type": "foo",
"object": {
"name": "Kazuki",
"token": "ac648657-797e-49f7-9563-150c9b5c2284"
}
}
{
"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
を正規表現を使って比較する際の指定例になります。
JSONAssert.assertEquals(expectedJson.toString(2), actualJson.toString(2),
new CustomComparator(JSONCompareMode.STRICT,
new Customization("object.token", new RegularExpressionValueMatcher<>()) // 期待値に指定したパターンを使ってマッチングする
)
);
また、以下のように検証時にパターンを指定することもできます。
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
を作成したいと思います。
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);
}
}
}
}
検証用に作成したテストケース
参考までに、検証時に作成したテストケースも貼り付けておきます。
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
を使って対象項目を指定するのが(ちょっと)つらいな〜と思い、期待値ファイルに正規表現を指定すれば自動的に適用できるようにしたかったためです。