Writing JSON REST services

JSON は今やマイクロサービス間の 共通言語 となっています。

このガイドでは、REST サービスが JSON ペイロードを利用および生成する方法を見ていきます。

there is another guide if you need a REST client (including support for JSON).

QuarkusでJSON RESTサービスを記述する方法を紹介します。 Quarkus REST(旧RESTEasy Reactive)のより詳細なガイドは こちら をご覧ください。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.16

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

アーキテクチャ

このガイドで構築されるアプリケーションは非常にシンプルです: ユーザーはフォームを使用してリストに要素を追加することができ、リストは更新されます。

ブラウザとサーバー間の情報はすべて JSON 形式になっています。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

git clone -b development https://github.com/quarkusio/quarkus-quickstarts.git で Git リポジトリーをクローンします。または、https://github.com/quarkusio/quarkus-quickstarts/archive/development.zip[アーカイブ] をダウンロードします。

ソリューションは rest-json-quickstart ディレクトリ にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:rest-json-quickstart \
    --extension='rest-jackson' \
    --no-code
cd rest-json-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus:quarkus-maven-plugin:999-SNAPSHOT:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -Dextensions='rest-jackson' \
    -DnoCode
cd rest-json-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=rest-json-quickstart"

このコマンドは、Quarkus REST/Jakarta RESTおよび Jackson エクステンションをインポートする新しいプロジェクトを生成し、 特に以下の依存関係を追加します:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-rest-jackson")

ユーザーエクスペリエンスを向上させるために、Quarkusは3つのJackson Java 8モジュール を登録しているので、手動で登録する必要はありません。

Quarkus also supports JSON-B so, if you prefer JSON-B over Jackson, you can create a project relying on the Quarkus REST JSON-B extension instead:

コマンドラインインタフェース
quarkus create app org.acme:rest-json-quickstart \
    --extension='rest-jsonb' \
    --no-code
cd rest-json-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus:quarkus-maven-plugin:999-SNAPSHOT:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-json-quickstart \
    -Dextensions='rest-jsonb' \
    -DnoCode
cd rest-json-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=rest-json-quickstart"

This command generates a new project importing the Quarkus REST/Jakarta REST and JSON-B extensions, and in particular adds the following dependency:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jsonb</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-rest-jsonb")

JSON-BとJSON-Pを使用する場合、 Json.createValue(…​) のような jakarta.json.Json が提供するショートカットメソッドを使用しないように注意してください。

現時点では、これらのメソッドを1回呼び出すと、 新しいJsonProvider が初期化 されるため、非常に時間がかかります。 Quarkusは、 quarkus-jsonp エクステンションの JsonProviderHolder クラスを介して、共有 JsonProvider インスタンスを提供します。

静的インポートとしてインポートすることで、コードを簡単にすることができます:

import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider;

[...]

    public void method() {
        jsonProvider().createValue("value");
    }

[...]

Quarkus RESTの詳細については、 専用ガイド を参照してください。

初めてのJSON RESTサービスの作成

この例では、果物のリストを管理するアプリケーションを作成します。

まず、以下のように Fruit Bean を作成してみましょう。

package org.acme.rest.json;

public class Fruit {

    public String name;
    public String description;

    public Fruit() {
    }

    public Fruit(String name, String description) {
        this.name = name;
        this.description = description;
    }
}

派手なことは何もありません。注意すべき重要なことは、デフォルトのコンストラクタを持つことはJSONシリアライズレイヤーで必須であるということです。

次に、 org.acme.rest.json.FruitResource クラスを以下のように作成します。

package org.acme.rest.json;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;

import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@Path("/fruits")
public class FruitResource {

    private Set<Fruit> fruits = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));

    public FruitResource() {
        fruits.add(new Fruit("Apple", "Winter fruit"));
        fruits.add(new Fruit("Pineapple", "Tropical fruit"));
    }

    @GET
    public Set<Fruit> list() {
        return fruits;
    }

    @POST
    public Set<Fruit> add(Fruit fruit) {
        fruits.add(fruit);
        return fruits;
    }

    @DELETE
    public Set<Fruit> delete(Fruit fruit) {
        fruits.removeIf(existingFruit -> existingFruit.name.contentEquals(fruit.name));
        return fruits;
    }
}

実装は非常に簡単で、Jakarta RESTアノテーションを使用してエンドポイントを定義するだけです。

The Fruit objects will be automatically serialized/deserialized by JSON-B or Jackson, depending on the extension you chose when initializing the project.

quarkus-rest-jackson や quarkus-rest-jsonb などのJSONエクステンションがインストールされている場合、 メディアタイプが @Produces や @Consumes アノテーションで明示的に設定されていない限り、Quarkusはほとんどの返り値にデフォルトで application/json メディアタイプを使用します(よく知られている String や File などのタイプには例外があり、デフォルトはそれぞれ text/plain や application/octet-stream です)。

JSON サポートの設定

Jackson

Quarkusでは、CDI経由で取得した(そしてQuarkusのエクステンションによって消費される)デフォルトのJackson ObjectMapper は、未知のプロパティーを無視するように設定されています( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 機能を無効にすることで)。

application.properties で quarkus.jackson.fail-on-unknown-properties=true を設定するか、 クラスごとに @JsonIgnoreProperties(ignoreUnknown = false) を設定することで、Jackson のデフォルトの動作を復元することができます。

さらに、 ObjectMapper 、日付と時刻をISO-8601でフォーマットするように設定されています( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS の機能を無効にすることで)。

application.properties で quarkus.jackson.fail-on-unknown-properties=true を設定するか、 @JsonIgnoreProperties(ignoreUnknown = false) を経由してクラスごとに設定することで、Jackson のデフォルトの動作を復元することができます。

Also, Quarkus makes it very easy to configure various Jackson settings via CDI beans. The simplest (and suggested) approach is to define a CDI bean of type io.quarkus.jackson.JsonMapperBuilderCustomizer inside of which any Jackson configuration can be applied.

カスタムモジュールを登録する必要がある場合の例は次のようになります。

import io.quarkus.jackson.JsonMapperBuilderCustomizer;
import jakarta.inject.Singleton;
import tools.jackson.databind.json.JsonMapper;

@Singleton
public class RegisterCustomModuleCustomizer implements JsonMapperBuilderCustomizer {

    public void customize(JsonMapper.Builder builder) {
        builder.addModule(new CustomModule());
    }
}

Users can even provide their own JsonMapper bean if they so choose. If this is done, it is very important to manually inject and apply all io.quarkus.jackson.JsonMapperBuilderCustomizer beans in the CDI producer that produces JsonMapper. Failure to do so will prevent Jackson specific customizations provided by various extensions from being applied.

import io.quarkus.arc.All;
import io.quarkus.jackson.JsonFactoryBuilderCustomizer;
import io.quarkus.jackson.JsonMapperBuilderCustomizer;
import java.util.List;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.databind.json.JsonMapper;

public class CustomObjectMapper {

    // Replaces the CDI producer for JsonMapper built into Quarkus
    @Singleton
    @Produces
    JsonMapper jsonMapper(@All List<JsonMapperBuilderCustomizer> customizers,
            @All List<JsonFactoryBuilderCustomizer> factoryCustomizers) {
        JsonFactoryBuilder factoryBuilder = JsonFactory.builder();
        factoryCustomizers.stream().sorted().forEach(customizer -> customizer.customize(factoryBuilder));

        JsonMapper.Builder builder = JsonMapper.builder(factoryBuilder.build());

        // Apply all JsonMapperBuilderCustomizer beans (including Quarkus customizers)
        customizers.stream().sorted().forEach(customizer -> customizer.customize(builder));

        return builder.build();
    }
}
Mixinサポート

Quarkus は、 io.quarkus.jackson.JacksonMixin アノテーションを介して、Jackson の Mixin サポートの登録を自動化します。 このアノテーションは、Jackson mixin として使用することを意図したクラスに配置できますが、カスタマイズすることを意図したクラスはアノテーションの値として定義されます。

JSON-B

上記のように、 Quarkus では、 quarkus-resteasy-jsonb エクステンションを使用することで、 Jackson の代わりに JSON-B を使用するオプションを提供しています。

前項と同様のアプローチで、 io.quarkus.jsonb.JsonbConfigCustomizer beanを使用してJSON-Bを設定することができます。

例えば、 FooSerializer という名前のカスタムシリアライザを com.example.Foo タイプで JSON-B で登録する必要がある場合、以下のような Bean を追加すれば十分です。

import io.quarkus.jsonb.JsonbConfigCustomizer;
import jakarta.inject.Singleton;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.serializer.JsonbSerializer;

@Singleton
public class FooSerializerRegistrationCustomizer implements JsonbConfigCustomizer {

    public void customize(JsonbConfig config) {
        config.withSerializers(new FooSerializer());
    }
}

より高度なオプションは, jakarta.json.bind.JsonbConfig の Bean を( Dependent スコープで)直接提供するか,極端な場合, jakarta.json.bind.Jsonb タイプの Bean を( Singleton スコープで)提供することでしょう。後者の方法を利用する場合、 jakarta.json.bind.Jsonb を生成するCDIプロデューサーにおいて、すべての io.quarkus.jsonb.JsonbConfigCustomizer Beanを手動で注入し適用することが非常に重要です。これを怠ると、さまざまなエクステンションが提供するJSON-B固有のカスタマイズが適用されなくなります。

import io.quarkus.jsonb.JsonbConfigCustomizer;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Instance;
import jakarta.json.bind.JsonbConfig;

public class CustomJsonbConfig {

    // Replaces the CDI producer for JsonbConfig built into Quarkus
    @Dependent
    JsonbConfig jsonConfig(Instance<JsonbConfigCustomizer> customizers) {
        JsonbConfig config = myJsonbConfig(); // Custom `JsonbConfig`

        // Apply all JsonbConfigCustomizer beans (incl. Quarkus)
        for (JsonbConfigCustomizer customizer : customizers) {
            customizer.customize(config);
        }

        return config;
    }
}

フロントエンドの作成

それでは、 FruitResource とやり取りするための簡単なウェブページを追加してみましょう。 Quarkusは自動的に、 META-INF/resources ディレクトリの下にある静的リソースを提供します。 src/main/resources/META-INF/resources ディレクトリに、この fruits.html ファイルのコンテンツを含む fruits.html ファイルを追加します。

これで、REST サービスと対話できるようになりました。

  • Quarkusを起動します。

    CLI
    quarkus dev
    Maven
    ./mvnw quarkus:dev
    Gradle
    ./gradlew --console=plain quarkusDev
  • http://localhost:8080/fruits.html をブラウザで開く

  • フォームを使って新しいフルーツをリストに追加します。

ネイティブ実行可能ファイルの構築

以下のコマンドでネイティブ実行可能ファイルをビルドすることができます。

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

実行は簡単で、 ./target/rest-json-quickstart-1.0-SNAPSHOT-runner を実行するだけです。

その後、ブラウザで http://localhost:8080/fruits.html を開いてアプリケーションを使用します。

シリアライゼーションについて

JSON シリアライズライブラリーは、 Java のリフレクションを使用してオブジェクトのプロパティーを取得してシリアライズします。

GraalVMでネイティブ実行可能ファイルを使用する場合、リフレクションで使用されるすべてのクラスを登録する必要があります。良いニュースは、Quarkusがほとんどの場合、その作業を代行してくれるということです。これまでのところ、 Fruit でさえ、リフレクトを使用するためのクラスを登録しておらず、すべてが正常に動作しています。

Quarkusは、RESTメソッドからシリアライズされた型を推論することができる場合に、何らかのマジックを実行します。以下のようなRESTメソッドがある場合、Quarkusは、 Fruit がシリアライズされると判断します。

@GET
public List<Fruit> list() {
    // ...
}

Quarkusは、ビルド時にRESTメソッドを分析することで、自動的にそのような処理を行ってくれます。

Jakarta RESTの世界でよく見られるもう一つのパターンは、 Response オブジェクトを使うことです。 Response には、いくつかの良い特典があります:

  • メソッドで何が起こるかによって異なるエンティティータイプを返すことができます (例えば Legume や Error )。

  • Response の属性を設定することができます (エラーが発生した時にステータスを知ることができます):

REST メソッドは次のようになります。

@GET
public Response list() {
    // ...
}

Response に含まれるタイプは情報がないため、Quarkusがビルド時に判断することはできません。この場合、Quarkusは必要なクラスを自動的に反映登録することができません。

これが次のセクションにつながります。

Response の利用

Fruit クラスと同じモデルに従って、JSON としてシリアライズされる Legume クラスを作成してみましょう。

package org.acme.rest.json;

public class Legume {

    public String name;
    public String description;

    public Legume() {
    }

    public Legume(String name, String description) {
        this.name = name;
        this.description = description;
    }
}

それでは、マメ科植物のリストを返すメソッドを一つだけ持つ LegumeResource REST サービスを作成してみましょう。

このメソッドは Legume のリストではなく Response を返ます。

package org.acme.rest.json;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

@Path("/legumes")
public class LegumeResource {

    private Set<Legume> legumes = Collections.synchronizedSet(new LinkedHashSet<>());

    public LegumeResource() {
        legumes.add(new Legume("Carrot", "Root vegetable, usually orange"));
        legumes.add(new Legume("Zucchini", "Summer squash"));
    }

    @GET
    public Response list() {
        return Response.ok(legumes).build();
    }
}

ここで、マメ科植物のリストを表示するための簡単なウェブページを追加してみましょう。 src/main/resources/META-INF/resources ディレクトリーに、この legumes.html ファイルの内容を含む legumes.html ファイルを追加します。

ブラウザーを開いて http://localhost:8080/legumes.html にアクセスすると、マメ科植物のリストが表示されます。

興味深いのは、アプリケーションをネイティブ実行可能ファイルとして実行するときです。

  • 以下でネイティブの実行可能ファイルを作成します。

    CLI
    quarkus build --native
    Maven
    ./mvnw install -Dnative
    Gradle
    ./gradlew build -Dquarkus.native.enabled=true
  • ./target/rest-json-quickstart-1.0-SNAPSHOT-runner で実行します。

  • ブラウザーを起動し、 http://localhost:8080/legumes.html にアクセスします。

そこにはマメ科類はありません。

上記のように、問題は Quarkus が、 REST エンドポイントを分析することで Legume クラスが何らかのリフレクションを必要とすることを判断できなかったことです。 JSON シリアライズライブラリーは、 Legume のフィールドのリストを取得しようとすると空のリストを取得するため、フィールドのデータをシリアライズしません。

現時点では、JSON-BやJacksonがクラスのフィールドのリストを取得しようとしたときに、そのクラスがリフレクションに登録されていない場合、例外はスローされません。GraalVMは単に空のフィールドのリストを返します。

将来的にはこの点が変わり、エラーがより明白になることが期待されます。

Legume クラスに @RegisterForReflection アノテーションを追加することで、手動で Legume を リフレクション用に登録することができます。

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class Legume {
    // ...
}
The @RegisterForReflection annotation instructs Quarkus to keep the class and its members during the native compilation. More details about the @RegisterForReflection annotation can be found on the native application tips page.

それを実行して、今までと同じ手順を踏んでみましょう。

  • Ctrl+C でアプリケーションを停止させます。

  • 以下でネイティブの実行可能ファイルを作成します。

    CLI
    quarkus build --native
    Maven
    ./mvnw install -Dnative
    Gradle
    ./gradlew build -Dquarkus.native.enabled=true
  • ./target/rest-json-quickstart-1.0-SNAPSHOT-runner で実行します。

  • ブラウザーを起動し、 http://localhost:8080/legumes.html にアクセスします。

今回はマメ科植物の一覧が表示されました。

リアクティブであること

非同期処理を処理するために リアクティブ型 を返すことができます。Quarkusでは、リアクティブで非同期なコードを書くために Mutiny の使用を推奨しています。

Quarkus RESTは当然Mutinyと統合されています。

そして、エンドポイントは Uni や Multi のインスタンスを返すことができます:

@GET
@Path("/{name}")
public Uni<Fruit> getOne(String name) {
    return findByName(name);
}

@GET
public Multi<Fruit> getAll() {
    return findAll();
}

単一の結果がある場合は Uni を使用します。 Multi は、非同期的に放出される可能性のある複数の項目がある場合に使用します。

Uni<Response> のように、 Uni と Response を使用して、非同期 HTTP レスポンスを返すことができます。

Mutiny についての詳細は、 Mutiny - 直感的なリアクティブプログラミングライブラリー に記載されています。

まとめ

Quarkusを使用したJSON RESTサービスの作成は、実績のあるよく知られたテクノロジーに依存しているため、簡単に行えます。

いつものように、 Quarkus は、アプリケーションをネイティブ実行可能ファイルとして実行する際に、内部の作業をさらに簡略化しています。

覚えておくべきことは一つだけあります。 Response を使用していて、Quarkus がシリアライズされているBeanを特定できない場合は、 @RegisterForReflection を使ってアノテーションを付ける必要があります。

As an alternative, you can use org.jboss.resteasy.reactive.RestResponse<T> instead of Response. Because RestResponse is strongly typed, Quarkus can determine the serialized type at build time and automatically registers it for reflection, eliminating the need for @RegisterForReflection.