😇

JavaのGetter/Setterの命名規約。uShapeのgetterはgetUShapeでいいの?Lombokは?Jacksonは?

2023/12/21に公開

Javaを使っている皆さん、Lombok使ってますか?Spring Boot使ってますか?
私は使っています。Spring Boot使っていたらこの組み合わせ多いと思います。
LombokとSpring Bootでリクエスト、レスポンスのJSONとオブジェクトの変換に利用されているJacksonの組み合わせで落とし穴にハマったので共有しようと思います。

遭遇した事象

まず、以下のコードを見てください。

@Getter
@Setter
public class DemoRequestDto {

    private String uShape;
    
}

生成されるgetter/setterのメソッド名は何だと思いますか?
特に設定をしていなければ、getUShapesetUShapeが生成されます。
実はこれはJava Beansのgetter/setterの命名規約に違反しています。
以下の画像を見てください。VSCodeではuShapeというフィールド名のgetter/setterとしてgetuShapesetuShapeを生成しています。

何となく話が見えてきたかと思いますが、以下のよくあるコードに対して、

@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ではないのです。

https://download.oracle.com/otn-pub/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/beans.101.pdf?AuthParam=1702575511_b45a34cfd4033787493e90978032689f

PDFの内容をまとめると、setFooBahというsetterに対応するJSONフィールドはfooBahsetZzsetURLURLが対応するようです。
uShapeのsetterはsetUShapeではなくsetuShapeなのです。(可読性低いな、、、)

ただ、実はJacksonのデフォルトの設定も標準では従っていないです。Jacksonのデフォルトの設定ではsetURLというsetterに対応するJSONフィールドはurlになってしまいます。setUShapeの場合はushapeになります。

問題を整理すると、

  1. uShapeに対してLombokが生成するメソッドはsetUShapeだが、jacksonはsetUShapeに対して、ushapeというフィールドを対応させるので、{"uShape": "a"}というJSONを正しくデシリアライズできない。
  2. 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を定義する

自分でsetuShapegetuShapeというメソッドを定義するだけです。

しかし、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が存在しません。なぜなら、getZgetzはどちらも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;

}

それでどうしたいの?

私としては、標準に合わせられるところは標準に合わせた方がいいと思うので、プロジェクトの立ち上げ時期なら設定を変更した方がいいと思います。ただし、その場合でもZTなどの大文字アルファベット1文字だけのJSONフィールドに対応するためには@JsonPropertyを使うしかないです。

しかし、URLZのように大文字から始まるフィールド名というのはJavaScript(つまりはJSON)でもJavaにおいてはあまり使うものではないため、遭遇する可能性が高いのはuShapeのように1文字目が小文字で2文字目が大文字のケースではないでしょうか。
そういう意味では、歴史の長いプロジェクトでもLombokの設定だけでも標準に合わせる設定にすると不必要な@JsonPropertyを削除できるようになる可能性は高いかなと思います。
既存の動作に影響を与えそうで怖い場合はprivate [a-z][A-Z].*;のように正規表現で検索してみるとある程度探せるかなと思います(全て洗い出せる保証はありませんが)。

余談

JavaBeansってそもそもなんだっけ?getter/setterの命名規約とどう関係があるの?って話ですが。
JavaBeansは元々UIを作るときに使っていたものらしいです。
そのJavaBeansで使われていたgetter/setterの規約がJavaでオブジェクトを生成して取り回すのに便利ということでJavaBeans自体が使われなくなった後も文化だけが残ったようです。

以下のブログにまとめられていてわかりやすかったです(一次文献を探すほどの興味がわかなかった)。

https://irof.hateblo.jp/entry/2019/07/25/132854

Discussion