Spring Boot: Hibernate ProxyとJacksonの「No serializer found」エラーを解決する3つの方法

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パターン利用
実装コスト 低い(設定のみ) 中(クラス作成が必要)
安全性 低い(内部構造が露出するリスク) 高い(公開フィールドを制御可能)
パフォーマンス 注意が必要(予期せぬクエリ発生) 最適化しやすい
Jackson Hibernate Module 公式ドキュメント

結論

No serializer found エラーは、HibernateとJacksonの責務の衝突によって発生します。短期的には Hibernate5JakartaModule でエラーを抑制できますが、長期的な保守性とパフォーマンスを考慮すると、DTOパターンを導入し、APIレスポンスとデータベースエンティティを明確に分離すること(Entity-DTOマッピング)がベストプラクティスです。

Post a Comment