JavaのGetter/Setterの命名規約。uShapeのgetterはgetUShapeでいいの?Lombokは?Jacksonは?
Javaを使っている皆さん、Lombok使ってますか?Spring Boot使ってますか?
私は使っています。Spring Boot使っていたらこの組み合わせ多いと思います。
LombokとSpring Bootでリクエスト、レスポンスのJSONとオブジェクトの変換に利用されているJacksonの組み合わせで落とし穴にハマったので共有しようと思います。
遭遇した事象
まず、以下のコードを見てください。
@Getter
@Setter
public class DemoRequestDto {
private String uShape;
}
生成されるgetter/setterのメソッド名は何だと思いますか?
特に設定をしていなければ、getUShape
とsetUShape
が生成されます。
実はこれはJava Beansのgetter/setterの命名規約に違反しています。
以下の画像を見てください。VSCodeではuShape
というフィールド名のgetter/setterとしてgetuShape
とsetuShape
を生成しています。
何となく話が見えてきたかと思いますが、以下のよくあるコードに対して、
@RestController
public class DemoController {
@PostMapping("/demo")
public void demo(@RequestBody DemoRequestDto request) {
System.out.println(request.getUShape());
}
}
curl -X POST http://localhost:8080/demo -d '{"uShape": "a"}' -H 'Content-Type:application/json'
を実行してみるとnull
と出力されます。
原因
以下のように書き換えてデバッガーで追ってみたところ、やはり、getuShape
の方が利用されています。
以下のPDFの8.8 Capitalization of inferred names.を読むと最初の2文字が大文字の場合はそのままになるらしいです。
つまり、setUShape
に対応するJSONフィールドはUShape
であってuShape
ではないのです。
PDFの内容をまとめると、setFooBah
というsetterに対応するJSONフィールドはfooBah
、setZ
はz
、setURL
はURL
が対応するようです。
uShape
のsetterはsetUShape
ではなくsetuShape
なのです。(可読性低いな、、、)
ただ、実はJacksonのデフォルトの設定も標準では従っていないです。Jacksonのデフォルトの設定ではsetURL
というsetterに対応するJSONフィールドはurl
になってしまいます。setUShape
の場合はushape
になります。
問題を整理すると、
-
uShape
に対してLombokが生成するメソッドはsetUShape
だが、jacksonはsetUShape
に対して、ushape
というフィールドを対応させるので、{"uShape": "a"}
というJSONを正しくデシリアライズできない。 -
URL
に対して、LombokはsetURL
というメソッドを生成しますが、jacksonはsetURL
というメソッドに対して、url
というフィールドを対応させるので{"URL":"b"}
というJSONを正しくデシリアライズできない。
解決策
まず、前提としてJSONのフィールド名を変えるということは考えません。外部APIを利用する場合など変えられない場合もあるからです。何とかして解決しないといけないのです。
問題1については複数の選択肢が考えられます。
- Lombokが標準的な規約に従うように設定する
- Lombokに頼らずJacksonが期待するように自分でgetter/setterを定義する
-
@JsonProperty
アノテーションでJSONフィールドとgetter/setterを対応づける
問題1
Lombokが標準的な規約に従うように設定する
Lombokの設定ファイルlombok.config
をプロジェクトのルートディレクトリに配置して、以下の設定を記載します。
lombok.accessors.capitalization = beanspec
これでLombokの生成するメソッドが標準的な規約に従うようになります。例えば、以下のクラスをコンパイルすると
@Getter
@Setter
public class DemoRequestDto {
private String uShape;
private String URL;
}
以下のようになります。setterがsetuShape
となり、規約に従うようになりました。
public class DemoRequestDto {
private String uShape;
private String URL;
public DemoRequestDto() {
}
public String getuShape() {
return this.uShape;
}
public String getURL() {
return this.URL;
}
public void setuShape(final String uShape) {
this.uShape = uShape;
}
public void setURL(final String URL) {
this.URL = URL;
}
}
Lombokに頼らずjacksonが期待するように自分でgetter/setterを定義する
自分でsetuShape
とgetuShape
というメソッドを定義するだけです。
しかし、Lombokがそのフィールドに対してgetter/setterを生成しないようにする必要があります。
具体的には以下のように@Getter(AccessLevel.NONE)
と@Setter(AccessLevel.NONE)
を指定すれば良いです。
@Getter
@Setter
public class DemoRequestDto {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private String uShape;
private String URL;
public String getuShape() {
return uShape;
}
public void setuShape(String uShape) {
this.uShape = uShape;
}
}
@JsonProperty
アノテーションでJSONフィールドとgetter/setterを対応づける
すでに結構な量のソースコードがあり、設定ファイルで全体に影響が出る可能性を抑えたい場合に便利です。
ただ、ぱっと見どうして@JsonProperty
つけているのか分かりにくいため、コードレビューなどで不毛なやり取りが発生するかもしれないです(発生した)。
以下のように@JsonProperty("uShape")
と記載すると、Lombokによって生成されるメソッドとJSONのuShape
フィールドがJacksonによって対応づけられるようになります。
@Getter
@Setter
public class DemoRequestDto {
@JsonProperty("uShape")
private String uShape;
private String URL;
}
AdHocな感じであまり好きにはなれそうにないですが、仕方ないですね。
問題2
問題2も複数の解決策があります。
- Jacksonが標準的な規約に従うように設定する
-
@JsonProperty
アノテーションでJSONフィールドとgetter/setterを対応づける
問題2の場合はJacksonのデフォルトの設定ではURL
フィールドに対応するgetter/setterは存在しないので、問題1のように自分で定義することはできません。
Jacksonが標準的な規約に従うように設定する
application.properties
に以下の設定を追加します。
spring.jackson.mapper.USE_STD_BEAN_NAMING=true
ただし、この設定をする上で気をつける必要があります。
以下のようにSpringのDIコンテナによって管理されているインスタンスを利用しないと設定は反映されません。
@Service
public class DemoService {
private ObjectMapper objectMapper;
@Autowired
public DemoService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
}
例えば、以下のように生成したインスタンスは通常の設定のままです。
ObjectMapper objectMapper = new ObjectMapper();
ただし、この方法ではZ
というJSONフィールドの場合には対応するGetter/Setterが存在しません。なぜなら、getZ
とgetz
はどちらもJSONフィールドz
に対応するからです。
そのため、@JsonProperty
を使う機会は依然として残るわけです。
@JsonProperty
アノテーションでJSONフィールドとgetter/setterを対応づける
Lomobok側の問題1と同じなので、説明は割愛しますが以下のように記載すると対応がつきます。
@Getter
@Setter
public class DemoRequestDto {
@JsonProperty("uShape")
private String uShape;
@JsonProperty("URL")
private String URL;
@JsonProperty("Z")
private String Z;
}
それでどうしたいの?
私としては、標準に合わせられるところは標準に合わせた方がいいと思うので、プロジェクトの立ち上げ時期なら設定を変更した方がいいと思います。ただし、その場合でもZ
やT
などの大文字アルファベット1文字だけのJSONフィールドに対応するためには@JsonProperty
を使うしかないです。
しかし、URL
やZ
のように大文字から始まるフィールド名というのはJavaScript(つまりはJSON)でもJavaにおいてはあまり使うものではないため、遭遇する可能性が高いのはuShape
のように1文字目が小文字で2文字目が大文字のケースではないでしょうか。
そういう意味では、歴史の長いプロジェクトでもLombokの設定だけでも標準に合わせる設定にすると不必要な@JsonProperty
を削除できるようになる可能性は高いかなと思います。
既存の動作に影響を与えそうで怖い場合はprivate [a-z][A-Z].*;
のように正規表現で検索してみるとある程度探せるかなと思います(全て洗い出せる保証はありませんが)。
余談
JavaBeansってそもそもなんだっけ?getter/setterの命名規約とどう関係があるの?って話ですが。
JavaBeansは元々UIを作るときに使っていたものらしいです。
そのJavaBeansで使われていたgetter/setterの規約がJavaでオブジェクトを生成して取り回すのに便利ということでJavaBeans自体が使われなくなった後も文化だけが残ったようです。
以下のブログにまとめられていてわかりやすかったです(一次文献を探すほどの興味がわかなかった)。
Discussion