0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

多くの 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 の型として、StringJsonArrayJsonPatch をサポートすることとします。

受け取った 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);
    }

とはいえ、JsonObjectJsonArray のインスタンスを用意するのがそもそも煩わしいので (その煩わしさを避けたいがために 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 の実装
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 の実装
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);

わずかな差ですが、このような積み重ねがコードの読み易さとメンテナンスのし易さに効いてきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?