Custom Scalar実装（GraphQL-SpringBoot）

# はじめに
GraphQLには標準のスカラー（`Boolean`, `Int`, `Float`, `String`, `ID`）のほか、独自に定義した任意のスカラーを使用することができる。Javaでの実装について調査しデモを作成したものをまとめた。[^1]

[^1]: 本稿作成時点では公式のサンプルがdeprecatedなコンストラクタを使っていたのでそれを回避する方法を調べた

# 実装
## スキーマ定義
* カスタムスカラーを宣言する
* 標準スカラーと同様に変数の型として使用する

```graphql
scalar ISODate

type Member {
	memberId: ID!
	name: String!
	age: Int
	birthday: ISODate
}
```

## DTO作成

typeに対応するDTOクラスを作成する

```java
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class Member {
	private String memberId;
	private String name;
	private Integer age;
	private LocalDate birthday;
}
```

## 変換定義実装

* スカラー名と変換定義の組み合わせを示す`GraphQLScalarType`インスタンスを作成する（シングルトン）
  * `name`にはスキーマで宣言したスカラー名を登録する
  * `coercing`には変換定義を実装したインスタンスを登録する
* 変換定義は`Coercing`インターフェースをimplmentsする
  * 今回はJava側の`java.time.LocalDate`をGraphQLの`ISODate`（実体は文字列）としてレスポンスに返すので、`Coercing`の仮型パラメータの"I"に`LocalDate`を"O"に`String`をそれぞれ指定する。

```java
import java.time.LocalDate;
import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import graphql.schema.GraphQLScalarType;

public class ISODateScalar {

	public static final GraphQLScalarType singleton = GraphQLScalarType.newScalar(
			).name("ISODate") // scalar名
			.coercing(new ISODateCoercing()) // 変換定義
			.build();

	// 変換定義の実装
	private static class ISODateCoercing implements Coercing<LocalDate, String> {

		@Override
		public String serialize(Object dataFetcherResult) throws CoercingSerializeException {
			if (dataFetcherResult instanceof LocalDate) {
				return dataFetcherResult.toString();
			}
			throw new CoercingSerializeException(dataFetcherResult + " is NOT LocalDate!");
		}

		@Override
		public LocalDate parseValue(Object input) throws CoercingParseValueException {
			try {
				return LocalDate.parse(input.toString());
			} catch (Exception e) {
				throw new CoercingParseValueException(e);
			}
		}

		@Override
		public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {
			try {
				return LocalDate.parse(((StringValue) input).getValue());
			} catch (Exception e) {
				throw new CoercingParseLiteralException(e);
			}
		}

	}
}
```

`serialize`はJavaの外にデータを送る際に使用される。なので`LocalDate`→`String`の変換を実装する。

`parseLiteral`はJavaの中にデータを取り込む際に使用される。なので`String`→`LocalDate`の返還を実装する。ただし、引数には`graphql.language.StringValue`（文字列の場合[^2]）でラップされた状態で渡されるため、`getValue`により`String`インスタンスを取り出す処理を挟む必要がある。

[^2]: 整数, 真偽値でリクエストが投げられた場合はそれぞれ`IntValue`, `BooleanValue`のインスタンスが渡される。

`parseValue`はいつ呼び出されるのかよくわからなかった……（教えてください）。公式のドキュメントを見る限りラップされていないインスタンスが渡される模様。

## 変換定義登録

上で作成したシングルトンをBeanとして登録する。

```java
@Configuration
public class ExtendedScalarConfiguration {
	@Bean
	public GraphQLScalarType isoDate() { // メソッド名は任意
		return ISODateScalar.singleton;
	}
}
```

# 参考サイト
* [GraphQLって何？ - Qiita](https://qiita.com/syu_chan_1005/items/3350f1d12c17a77e98c7)
* [GraphQL Java - Scalar]( https://www.graphql-java.com/documentation/v13/scalars/ )
* [graphql-java/graphql-java-extended-scalars]( https://github.com/graphql-java/graphql-java-extended-scalars )
* [Spring Boot 2 / GraphQL Extended Scalars not working: Exception “Cannot construct instance” “cannot deserialize”]( https://stackoverflow.com/questions/57070610/spring-boot-2-graphql-extended-scalars-not-working-exception-cannot-construc )