API開発中に突然、HTTP 500エラーと共に com.fasterxml.jackson.databind.exc.InvalidDefinitionException が発生し、ログに No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor というメッセージが出力されることがあります。これは、Spring BootコントローラーがJPAエンティティを直接JSONとして返そうとした際に、Hibernateの遅延ローディング(Lazy Loading)機構とJacksonのシリアライズ処理が衝突して発生する典型的な問題です。
なぜByteBuddyInterceptorがエラーの原因になるのか
以前、高負荷なECサイトのバックエンドをリファクタリングしていた際、このエラーが頻発しました。原因はHibernateのパフォーマンス最適化機能にあります。
JPAで FetchType.LAZY を設定している関連エンティティを取得する際、Hibernateは実際のデータではなく、プロキシオブジェクトを生成します。Spring Bootのデフォルトでは、このプロキシの実装にByteBuddyが使用されます。
InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
JacksonはデフォルトでJava Bean規約(Getterメソッドの存在など)に従ってJSON化を試みますが、Hibernateが生成したプロキシ内部の ByteBuddyInterceptor クラスには公開プロパティがないため、Jacksonは「何をシリアライズすればいいか分からない」と判断し、例外をスローします。
解決策1: Hibernate5JakartaModuleの導入(即効性あり)
既存のコードベースを大きく変更せずに修正したい場合、最も手軽なのはJacksonにHibernateのデータ型を理解させることです。これには jackson-datatype-hibernate5-jakarta モジュールを使用します(Spring Boot 3系の場合)。
まず、build.gradle または pom.xml に依存関係を追加します。
// Gradle (Spring Boot 3.x / Hibernate 6.x)
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-jakarta'
次に、設定クラスでこのモジュールをBeanとして登録します。Spring Bootのオートコンフィギュレーションがこれを検出し、ObjectMapper に自動的に登録してくれます。
@Configuration
public class JacksonConfig {
// Hibernateの遅延ロードされたプロパティを無視、または強制ロードさせる設定
@Bean
public com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule hibernate5Module() {
com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule module =
new com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule();
// オプション: 遅延ロードのフィールドを強制的にロードする場合(N+1問題に注意)
// module.enable(com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING);
return module;
}
}
FORCE_LAZY_LOADING を有効にすると、レスポンス生成時に全ての遅延ロードフィールドへのクエリが発行され、深刻なN+1問題を引き起こす可能性があります。基本的にはデフォルト設定(遅延ロード項目はnullとして扱う)を推奨します。
解決策2: DTO (Data Transfer Object) パターンの採用(推奨)
解決策1はあくまで対症療法です。エンティティを直接コントローラーの戻り値として公開することは、APIの契約とデータベース設計が密結合してしまうため、アーキテクチャ上のアンチパターンとされています。
最も堅牢な解決策は、必要なデータだけを持つ DTO (Data Transfer Object) を作成し、エンティティからDTOへ変換して返却することです。
// 1. レスポンス用のDTOを作成 (Java 16+ Recordの例)
public record UserResponseDto(
Long id,
String username,
String email
) {}
// 2. ServiceまたはControllerで変換
@GetMapping("/{id}")
public UserResponseDto getUser(@PathVariable Long id) {
UserEntity entity = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User not found"));
// 必要なフィールドのみをマッピング(プロキシ問題は発生しない)
return new UserResponseDto(
entity.getId(),
entity.getUsername(),
entity.getEmail()
);
}
DTOを使用することで、Hibernateのプロキシクラスではなく、純粋なPOJO(またはRecord)がJacksonに渡されるため、シリアライズエラーは100%回避できます。
| 比較項目 | Hibernate Module利用 | DTOパターン利用 |
|---|---|---|
| 実装コスト | 低い(設定のみ) | 中(クラス作成が必要) |
| 安全性 | 低い(内部構造が露出するリスク) | 高い(公開フィールドを制御可能) |
| パフォーマンス | 注意が必要(予期せぬクエリ発生) | 最適化しやすい |
結論
No serializer found エラーは、HibernateとJacksonの責務の衝突によって発生します。短期的には Hibernate5JakartaModule でエラーを抑制できますが、長期的な保守性とパフォーマンスを考慮すると、DTOパターンを導入し、APIレスポンスとデータベースエンティティを明確に分離すること(Entity-DTOマッピング)がベストプラクティスです。
Post a Comment