Spring Data JPA で hibernateLazyInitializer を Jackson が JSON シリアライズできなくてエラーになる

## 概要

- Spring Data JPA 使用時に Hibernate がエンティティのプロキシオブジェクトに hibernateLazyInitializer というフィールドを追加してしまう
- Jackson が hibernateLazyInitializer を JSON シリアライズできずにエラーになってしまう
- いくつかの対策方法を示す

## エラーメッセージ例

環境: Java 11 + Spring Boot 2.2.6 + Spring Data JPA 2.2.6 + Hibernate ORM 5.4.12.Final

Jackson が hibernateLazyInitializer を JSON にシリアライズできなくてエラーになっている。

> Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.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) (through reference chain: java.util.ArrayList[0]->com.example.Counter\$HibernateProxy$YAFGFxdB["hibernateLazyInitializer"])] with root cause

## 今回の JPA エンティティクラス

```java
package com.example;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Data // Lombok で getter setter など便利なメソッドを自動生成
@Entity // JPA エンティティとして扱う
public class Counter {

  // カウンター名
  @Id // JPA にこの変数をオブジェクトの ID だと認識させる
  private String name;

  // カウント数
  private int count;

  // 更新日時
  private LocalDateTime updatedAt;
}
```

## Hibernate によるプロキシクラスの中身を調べる

以下のようにクラスを調べるメソッドを作成。

```java
private static String getClassInfo(Class cls) {
  var s = new ArrayList<String>();
  s.add("Class Name: " + cls.getName());
  s.add("Super Class: " + cls.getSuperclass().getName());
  s.add("Methods: " + Arrays.stream(cls.getMethods()).map(m -> m.getName()).collect(Collectors.joining(", ")));
  s.add("Fields: " + Arrays.stream(cls.getFields()).map(f -> f.getName()).collect(Collectors.joining(", ")));
  s.add("Declared Methods: " + Arrays.stream(cls.getDeclaredMethods()).map(m -> m.getName()).collect(Collectors.joining(", ")));
  s.add("Declared Fields: " + Arrays.stream(cls.getDeclaredFields()).map(f -> f.getName()).collect(Collectors.joining(", ")));
  return String.join("\n", s);
}
```

このメソッドに Hibernate が生成したオブジェクトの Class オブジェクトを指定した結果の出力例。
エンティティクラスを親クラスとして、getHibernateLazyInitializer メソッドが増えているのがわかる。

```
Class Name: com.example.Counter$HibernateProxy$AkTOy5Fs
Super Class: com.example.Counter
Methods: equals, toString, hashCode, getName, setName, writeReplace, getUpdatedAt, getCount, getHibernateLazyInitializer, setCount, setUpdatedAt, $$_hibernate_set_interceptor, wait, wait, wait, getClass, notify, notifyAll
Fields: INTERCEPTOR_FIELD_NAME
Declared Methods: equals, toString, hashCode, clone, getName, setName, writeReplace, getUpdatedAt, getCount, canEqual, getHibernateLazyInitializer, setCount, setUpdatedAt, $$_hibernate_set_interceptor
Declared Fields: $$_hibernate_interceptor, cachedValue$qiSxWcNz$dgag0r1, cachedValue$qiSxWcNz$o23rrk2, cachedValue$qiSxWcNz$fktln90, cachedValue$qiSxWcNz$7m9oaq0, cachedValue$qiSxWcNz$1ev2u10, cachedValue$qiSxWcNz$bolum62, cachedValue$qiSxWcNz$csesgl2, cachedValue$qiSxWcNz$iiddvi2, cachedValue$qiSxWcNz$14i7kh1, cachedValue$qiSxWcNz$hm0jak2, cachedValue$qiSxWcNz$c92ibj2, cachedValue$qiSxWcNz$gpia792, cachedValue$qiSxWcNz$ca4pq83
```

## 対策1: spring.jackson.serialization.fail-on-empty-beans=false

application.properties に spring.jackson.serialization.fail-on-empty-beans=false を記述する。

```application.properties
spring.jackson.serialization.fail-on-empty-beans=false
```

例外が発生せず hibernateLazyInitializer が空のオブジェクトで出力されるようになる。

JSON 出力例。

```json
[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T18:39:57.861259","hibernateLazyInitializer":{}}]  
```

[SerializationFeature \(jackson\-databind 2\.10\.0 API\)](https://fasterxml.github.io/jackson-databind/javadoc/2.10/com/fasterxml/jackson/databind/SerializationFeature.html#FAIL_ON_EMPTY_BEANS)

> public static final SerializationFeature FAIL_ON_EMPTY_BEANS
> 
> Feature that determines what happens when no accessors are found for a type (and there are no annotations to indicate it is meant to be serialized). If enabled (default), an exception is thrown to indicate these as non-serializable types; if disabled, they are serialized as empty Objects, i.e. without any properties.
> Note that empty types that this feature has only effect on those "empty" beans that do not have any recognized annotations (like @JsonSerialize): ones that do have annotations do not result in an exception being thrown.
> 
> Feature is enabled by default.

## 対策2: @JsonIgnoreProperties アノテーション

エンティティクラスに @JsonIgnoreProperties({"hibernateLazyInitializer"}) を付加することで該当の項目を JSON シリアライズしないようにする。

```java
package com.example;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Data // Lombok で getter setter など便利なメソッドを自動生成
@Entity // JPA エンティティとして扱う
@JsonIgnoreProperties({"hibernateLazyInitializer"})
public class Counter {

  // カウンター名
  @Id // JPA にこの変数をオブジェクトの ID だと認識させる
  private String name;

  // カウント数
  private int count;

  // 更新日時
  private LocalDateTime updatedAt;
}
```

[JsonIgnoreProperties \(Jackson\-annotations 2\.10\.0 API\)](https://fasterxml.github.io/jackson-annotations/javadoc/2.10/com/fasterxml/jackson/annotation/JsonIgnoreProperties.html)

> Annotation that can be used to either suppress serialization of properties (during serialization), or ignore processing of JSON properties read (during deserialization).

JSON 出力例。

```json
[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T18:45:05.009029"}]
```

## 対策3: jackson-datatype-hibernate を導入する

jackson-datatype-hibernate は Hibernate の JSON シリアライズ・デシリアライズをサポートするライブラリ。

[GitHub \- FasterXML/jackson\-datatype\-hibernate: Add\-on module for Jackson JSON processor which handles Hibernate \(http://www\.hibernate\.org/\) datatypes; and specifically aspects of lazy\-loading](https://github.com/FasterXML/jackson-datatype-hibernate)

> Project to build Jackson module (jar) to support JSON serialization and deserialization of Hibernate (http://hibernate.org) specific datatypes and properties; especially lazy-loading aspects.

Gradle の場合は build.gradle の dependencies に jackson-datatype-hibernate5 を追加することで Hibernate 5 に対応したライブラリを導入できる。

```groovy
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
```

また Jackson の ObjectMapper オブジェクトを生成するための設定クラスが必要。
jackson-datatype-hibernate5 を導入することで Hibernate5Module クラスが使えるようになる。

```java
package com.example;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class MyConfig {

  @Bean
  public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {

    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();

    // Hibernate 5 使用時のサポート
    builder.modulesToInstall(new Hibernate5Module());

    // Spring Boot のデフォルト設定に合わせる
    builder.featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION);
    builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    return builder;
  }
}
```

Spring Boot が ObjectMapper を生成する際のデフォルト設定値。

[Spring Boot Reference Documentation - “How\-to” Guides - 4.3. Customize the Jackson ObjectMapper](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/html/howto.html#howto-customize-the-jackson-objectmapper)

> The ObjectMapper (or XmlMapper for Jackson XML converter) instance (created by default) has the following customized properties:
> 
> ・MapperFeature.DEFAULT_VIEW_INCLUSION is disabled
> ・DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES is disabled
> ・SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is disabled

JSON 出力例。

```json
[{"name":"mycounter","count":1,"updatedAt":"2020-05-09T20:10:59.749087"}]
```

## 参考資料

- [Spring Boot Reference Documentation - “How\-to” Guides - 4.3. Customize the Jackson ObjectMapper](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/html/howto.html#howto-customize-the-jackson-objectmapper)
- [Spring Boot Reference Documentation - Spring Boot Features - 2.8.6. Relaxed Binding](https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/html/spring-boot-features.html#boot-features-external-config-relaxed-binding)
- [hibernate \- Converting Lazy Loaded object to JSON in spring boot jpa \- Stack Overflow](https://stackoverflow.com/questions/55942356/converting-lazy-loaded-object-to-json-in-spring-boot-jpa)
- [java \- Strange Jackson exception being thrown when serializing Hibernate object \- Stack Overflow](https://stackoverflow.com/questions/4362104/strange-jackson-exception-being-thrown-when-serializing-hibernate-object)