はじめに
多くの JSON プロセッサを使った経験から、個人的には GSON が一番使い勝手が良いと感じています。しかし、GSON は JSON Pointer (RFC 6901) や JSON Patch (RFC 6902) をサポートしていません。ですので、JSON Pointer や JSON Patch を扱いたい場合は、Jakarta JSON Processing で定義されている jakarta.json パッケージを使うことにしています。
ただ、jakarta.json パッケージのクラス群を直接使うとコード量が多くなってしまいます。そこで本記事では、 JSON Patch の処理を少ないコード量で書けるようにするためのユーティリティクラス JPatch を書いていきます。
設計
JPatch への入力は、JSON Patch とその適用対象である JSON の二つです。JPatch からの出力は、JSON Patch を適用した後の JSON です。目指す利用方法を仮想コードで示すと次のようになります。
パッチ適用後のJSON = new JPatch(JSONパッチ).apply(パッチ適用前のJSON);
実装
コンストラクタ
コンストラクタが引数で受け取る JSON Patch の型として、String、JsonArray、JsonPatch をサポートすることとします。
受け取った JSON Patch は、JsonPatch インスタンスに変換して patch フィールドにセットし、後の apply メソッドコール時に利用できるようにしておきます。
以下、コンストラクタ、および、そこから呼ばれる内部メソッド群の実装です。
public class JPatch
{
private final JsonPatch patch;
public JPatch(String patch)
{
// 文字列を JSON 配列とみなして JsonArray へと変換し、
// this(JsonArray) を呼ぶ
this(createJsonArray(patch));
}
public JPatch(JsonArray patch)
{
// JSON 配列を JSON Patch とみなして JsonPatch へと変換し、
// this(JsonPatch) を呼ぶ
this(Json.createPatch(patch));
}
public JPatch(JsonPatch patch)
{
this.patch = patch;
}
private static JsonArray createJsonArray(String json)
{
// 文字列を JSON として読むためのリーダーを作成する
JsonReader reader = createJsonReader(json);
// リーダーから JSON 配列を読む
return reader.readArray();
}
private static JsonReader createJsonReader(String json)
{
// 文字列に Reader インターフェースでアクセスできるようにする
Reader reader = new StringReader(json);
// Reader から JsonReader を作成する
return Json.createReader(reader);
}
パッチ取得
コンストラクタの内部で生成した JsonPatch インスタンスにアクセスするためのメソッドを用意しておきます。
public JsonPatch getPatch()
{
return patch;
}
パッチ適用
JSON Patch の適用対象として、JSON オブジェクトと JSON 配列のどちらも選べるよう、apply メソッドの引数は、JsonObject インターフェースと JsonArray インターフェースの共通の親インターフェースである JsonStructure とします。
public <T extends JsonStructure> T apply(T input)
{
// パッチを適用する
return getPatch().apply(input);
}
とはいえ、JsonObject や JsonArray のインスタンスを用意するのがそもそも煩わしいので (その煩わしさを避けたいがために JPatch ユーティリティを書いているので)、JSON Patch を適用するための次のメソッド群も提供することにします。
JsonObject apply(Map<String, ?> input)JsonObject applyToObject(String input)JsonArray apply(Collection<?> input)JsonArray applyToArray(String input)
これらのメソッド群の実装は次のようになります。
public JsonObject apply(Map<String, ?> input)
{
// Map から JsonObject を作成する
JsonObject object = createJsonObject(input);
// パッチを適用する
return getPatch().apply(object);
}
private static JsonObject createJsonObject(Map<String, ?> input)
{
// Map から JsonObject を作成する
return Json.createObjectBuilder(input).build();
}
public JsonObject applyToObject(String input)
{
// 文字列を JSON オブジェクトとして解釈する
JsonObject object = createJsonObject(input);
// パッチを適用する
return getPatch().apply(object);
}
private static JsonObject createJsonObject(String json)
{
// 文字列を JSON として読むためのリーダーを作成する
JsonReader reader = createJsonReader(json);
// リーダーから JSON オブジェクトを読む
return reader.readObject();
}
public JsonArray apply(Collection<?> input)
{
// Collection から JsonArray を作成する
JsonArray array = createJsonArray(input);
// パッチを適用する
return getPatch().apply(array);
}
private static JsonArray createJsonArray(Collection<?> input)
{
// Collection から JsonArray を作成する
return Json.createArrayBuilder(input).build();
}
public JsonArray applyToArray(String input)
{
// 文字列を JSON 配列として解釈する
JsonArray array = createJsonArray(input);
// パッチを適用する
return getPatch().apply(array);
}
実装まとめ
JPatch.java の実装
package com.example;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collection;
import java.util.Map;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonPatch;
import jakarta.json.JsonReader;
import jakarta.json.JsonStructure;
public class JPatch
{
private final JsonPatch patch;
public JPatch(String patch)
{
// 文字列を JSON 配列とみなして JsonArray へと変換し、
// this(JsonArray) を呼ぶ
this(createJsonArray(patch));
}
public JPatch(JsonArray patch)
{
// JSON 配列を JSON Patch とみなして JsonPatch へと変換し、
// this(JsonPatch) を呼ぶ
this(Json.createPatch(patch));
}
public JPatch(JsonPatch patch)
{
this.patch = patch;
}
private static JsonArray createJsonArray(String json)
{
// 文字列を JSON として読むためのリーダーを作成する
JsonReader reader = createJsonReader(json);
// リーダーから JSON 配列を読む
return reader.readArray();
}
private static JsonReader createJsonReader(String json)
{
// 文字列に Reader インターフェースでアクセスできるようにする
Reader reader = new StringReader(json);
// Reader から JsonReader を作成する
return Json.createReader(reader);
}
public JsonPatch getPatch()
{
return patch;
}
public <T extends JsonStructure> T apply(T input)
{
// パッチを適用する
return getPatch().apply(input);
}
public JsonObject apply(Map<String, ?> input)
{
// Map から JsonObject を作成する
JsonObject object = createJsonObject(input);
// パッチを適用する
return getPatch().apply(object);
}
private static JsonObject createJsonObject(Map<String, ?> input)
{
// Map から JsonObject を作成する
return Json.createObjectBuilder(input).build();
}
public JsonObject applyToObject(String input)
{
// 文字列を JSON オブジェクトとして解釈する
JsonObject object = createJsonObject(input);
// パッチを適用する
return getPatch().apply(object);
}
private static JsonObject createJsonObject(String json)
{
// 文字列を JSON として読むためのリーダーを作成する
JsonReader reader = createJsonReader(json);
// リーダーから JSON オブジェクトを読む
return reader.readObject();
}
public JsonArray apply(Collection<?> input)
{
// Collection から JsonArray を作成する
JsonArray array = createJsonArray(input);
// パッチを適用する
return getPatch().apply(array);
}
private static JsonArray createJsonArray(Collection<?> input)
{
// Collection から JsonArray を作成する
return Json.createArrayBuilder(input).build();
}
public JsonArray applyToArray(String input)
{
// 文字列を JSON 配列として解釈する
JsonArray array = createJsonArray(input);
// パッチを適用する
return getPatch().apply(array);
}
}
使用例
下記は JPatch クラスの使用例で、JUnit のテストにもなっています。
JPatchTest.java の実装
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import jakarta.json.JsonObject;
import org.junit.jupiter.api.Test;
public class JPatchTest
{
@Test
public void test()
{
// JSON Patch 適用前の JSON
String input = """
{
"a": 1,
"b": 2
}
""";
// JSON Patch
//
// 1. "a" の値を 100 で置き換える (replace)
// 2. "b" を取り除く (remove)
// 3. "c":3 を追加する (add)
//
String patch = """
[
{"op":"replace", "path":"/a", "value":100},
{"op":"remove", "path":"/b" },
{"op":"add", "path":"/c", "value":3}
]
""";
// JSON Patch を適用する
JsonObject output = new JPatch(patch).applyToObject(input);
// 以下、テスト
// "a" が含まれていなければならない
assertTrue(output.containsKey("a"), "The key \"a\" must be contained.");
// "a" の値は 100 でなければならない
assertEquals(output.getInt("a"), 100, "The value of \"a\" must be 100.");
// "b" は含まれていてはならない
assertFalse(output.containsKey("b"), "The key \"b\" must not be contained.");
// "c" が含まれていなければならない
assertTrue(output.containsKey("c"), "The key \"c\" must be contained.");
// "c" の値は 3 でなければならない
assertEquals(output.getInt("c"), 3, "The value of \"c\" must be 3.");
}
}
テスト
pom.xml ファイルを用意し、
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jpatch</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jakarta.jakartaee-bom.version>10.0.0</jakarta.jakartaee-bom.version>
<junit-bom.version>5.14.1</junit-bom.version>
<maven-compiler-plugin.version>3.14.1</maven-compiler-plugin.version>
<maven-compiler-plugin.release>17</maven-compiler-plugin.release>
<parsson.version>1.1.7</parsson.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Jakarta EE -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-bom</artifactId>
<version>${jakarta.jakartaee-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Jakarta JSON Processing -->
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.parsson</groupId>
<artifactId>parsson</artifactId>
<version>${parsson.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${maven-compiler-plugin.release}</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
ファイル群を次のように配置すれば、
.
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── example
│ └── JPatch.java
└── test
└── java
└── com
└── example
└── JPatchTest.java
mvn test でテストを実行できます。
mvn test
おわりに
JSON Patch (RFC 6902) を表す String インスタンスから JsonPatch インスタンスを生成する処理は次のように書けますが、
JsonPatch patch =
Json.createPatch(
Json.createReader(
new StringReader(json)).readArray());
本記事では、わざわざ JPatch というユーティリティクラスを作り、次のように書けるようにしました。
JPatch patch = new JPatch(json);
わずかな差ですが、このような積み重ねがコードの読み易さとメンテナンスのし易さに効いてきます。