Geometry型のオブジェクトをJacksonでJson化する時の循環参照を回避する

# 前提条件

- Geometry：`org.locationtech.jts.geom`パッケージを利用
- spring boot 2.2.5.RELEASE
- spring data jpa
- jackson
- kotlin

`Spring data JPA`を使って`OneToMany`、`ManyToOne`なリレーションを貼ったentityをJsonでレスポンスするようなAPIを作ろうとしたら、恐ろしい見た目のJsonが出来上がってしまったので対処法を調べました。

# まずは失敗結果から

## 依存関係

```build.gradle.kts
dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-data-jpa")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	runtimeOnly("org.postgresql:postgresql")
	implementation("net.postgis:postgis-jdbc:2.2.1")
	implementation("org.hibernate:hibernate-spatial:5.4.12.Final")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.+")
	testImplementation("org.springframework.boot:spring-boot-starter-test") {
		exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
	}
	implementation("org.springframework.boot:spring-boot-devtools")
	implementation("org.flywaydb:flyway-core")
}
```

今回の趣旨に関係しそうなものとしては、Geometryクラスを扱える様にするために`hibernate-spatial`を入れています。


## エンティティ

```kotlin
package entity


import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import org.hibernate.annotations.Where
import org.locationtech.jts.geom.MultiPoint
import javax.persistence.*

//駅
@Entity
@Table(name = "stations")
@Where(clause="is_deleted = false")
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator::class)
data class Station(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "stations_id_seq")
        val id: Long? = null,
        var name: String? = null,

        //リレーション
        @OneToMany(mappedBy = "station")
        var stationSections: List<StationSection>? = null
): CommonEntity()

//駅区画
@Entity
@Table(name = "station_sections")
@Where(clause="is_deleted = false")
data class StationSection(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "station_sections_id_seq")
        val id: Long? = null,
        var sectionPoints: MultiPoint? = null,

        //リレーション
        @ManyToOne
        var station: Station? = null
): CommonEntity()
```

`@JsonIdentityInfo`アノテーションは`OneToMany`、`ManyToOne`なリレーション関係をjson化する時に相互参照になってしまう現象を防いでくれるものです。
→ <a href="https://qiita.com/sengoku/items/56bc6319759fee6d15e3" target="_blank">こちら</a>を参考にさせていただきました。

継承している`CommonEntity`クラスはcreated_at、updated_atなどのテーブル共通のカラムが定義してあるだけなので省略します。

## JpaRepository、Serviceクラス、Controllerクラス

これらは大したことを書いていないので省略します。

- JpaRepository：基本的な使い方で、interface定義しただけ
- Serviceクラス：repositoryを使って検索しているだけ
- Controllerクラス：Serviceクラスのメソッドの戻り値をreturnしてるだけ

## APIを実行

ブラウザからリクエストしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510032/7fb1fd9f-e972-f982-d6fb-3b508e6fd1e2.png)

生データを表示したのがこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510032/e86164b0-dafc-6f7c-89fb-f382182d2242.png)

地獄絵図w

どうやら`MultiPoint`型のsectionPointsをjson化する時のシリアライズあたりで無限ループ的な現象になっている様な・・・。

# 解決方法

色々ググっていたら、jackson標準のシリアライズクラスだとGeometryに対応していないとかなんとか？っぽいです。
シリアライズクラスとでシリアライズクラスを自作しなさいとか書いてるのも見ましたが、それは面倒くさいので最終手段にとっておいて、他の方法を探しました。

## その１（解決しなかった）

`com.bedatadriven jackson-datatype-jts`を入れればいけるよという記事がありましたが、これはGeometryのパッケージに`com.vividsolutions.jts.geom`を使っている場合で、今回のケースではClassCastExceptionが起きます。


## その２（こっちで解決）

Stackoverflowに<a href="https://mvnrepository.com/artifact/com.graphhopper.external/jackson-datatype-jts" target="_blank">Jackson DataType JTS</a>を組み込めばいけるよとか書いてあったので入れてみました。

### 依存関係

```build.gradle.kt
dependencies {
	/**
	 *  省略
	 */  

	//これを追記しました
	implementation("com.graphhopper.external:jackson-datatype-jts:1.0-2.7")
}
```

### エンティティ

```kotlin
package entity

import com.bedatadriven.jackson.datatype.jts.serialization.GeometryDeserializer
import com.bedatadriven.jackson.datatype.jts.serialization.GeometrySerializer
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import org.hibernate.annotations.Where
import org.locationtech.jts.geom.MultiPoint
import javax.persistence.*

//駅
@Entity
@Table(name = "stations")
@Where(clause="is_deleted = false")
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator::class)
data class Station(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "stations_id_seq")
        val id: Long? = null,
        var name: String? = null,

        //リレーション
        @OneToMany(mappedBy = "station")
        var stationSections: List<StationSection>? = null
): CommonEntity()

//駅区画
@Entity
@Table(name = "station_sections")
@Where(clause="is_deleted = false")
data class StationSection(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "station_sections_id_seq")
        val id: Long? = null,
        @JsonSerialize(using = GeometrySerializer::class)
        @JsonDeserialize(using = GeometryDeserializer::class)
        var sectionPoints: MultiPoint? = null,

        //リレーション
        @ManyToOne
        var station: Station? = null
): CommonEntity()
```

`MultiPoint`型の変数に`@JsonSerialize`アノテーションを付けて、using変数に今回追加した`GeometrySerializer`クラスを指定してシリアライズします。
デシリアライズも同じ要領で指定します。


### APIを実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510032/00073c65-f303-e789-f65c-57ca160ef3f5.png)


無事にJsonをレスポンスできました！

# 参考記事

- <a href="https://qiita.com/sengoku/items/56bc6319759fee6d15e3" target="_blank">「[Java] エンティティのJSON化において、無限ループを防ぐ」</a>

- Qiita含め、他多数のサイトを参考にさせていただきました。