【Jackson】共通のunboxするシリアライズ処理を実装する

# TL;DR
- 当該の抽象型向けの`StdSerializer`を定義し、適切に登録することで実現できる
- 中身の型ごとのシリアライズは、`SerializerProvider::defaultSerializeValue`で行える

# やること
以下のような、値をラップする抽象型が有るとします。
用途としては、`DDD`でいう値オブジェクトを定義する時、共通の振る舞いを持たせるために基底となる型を作る場合を想定しています。

```java
package com.wrongwrong.values;

public abstract class AbstractValue<T extends Comparable<T>> {
    public abstract T getValue();
}
```

このクラスは、以下のように継承されて使われます。

```java:intでの継承例
package com.wrongwrong.values;

public class Foo extends AbstractValue<Integer> {
    private final int value;

    public Foo(int value) { this.value = value; }

    @Override
    public Integer getValue() { return value; }
}
```

この型を`foo`というフィールドに設定した場合、そのまま`JSON`にシリアライズした結果は以下のようになります。

```json
{
  "foo": {
    "value": 10
  }
}
```

一方、この型はあくまでサーバーサイドの都合でラップするためのものであるため、フロントへ渡すシリアライズ結果は以下のように`unbox`された形になっている方が望ましいです。

```json
{
  "foo": 10
}
```

この`unbox`するシリアライズ処理は、個別に書くと大変で、かといって`AbstractValue`側に`JsonValue`アノテーションを設定するのも不適切です[^huteki]。
そこで、`Jackson`を用いて共通の`unbox`するシリアライズ処理を実装します。

[^huteki]: `AbstractValue`に表示用ロジックが入ってしまうのは責務が漏れていると言えるため。

# やり方
ここでは`AbstractValue`をシリアライズする`StdSerializer`を実装し、`SimpleModule`を介して`ObjectMapper`に登録する方法を紹介します。

## シリアライザーの実装
シリアライザーの実装は以下の通りです。

```java
package com.wrongwrong.ser;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.wrongwrong.values.AbstractValue;

import java.io.IOException;

public class AbstractValueSerializer extends StdSerializer<AbstractValue<?>> {
    public AbstractValueSerializer() {
        // AbstractValueがジェネリクスを持つ関係で、dummyフラグを設定する必要が有る
        super(AbstractValue.class, true);
    }

    @Override
    public void serialize(AbstractValue<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        Object unboxed = value.getValue();
        provider.defaultSerializeValue(unboxed, gen)
    }
}
```

### 注意点
`Java`上の値から`JSON`へシリアライズする時には、以下のように様々なロジックが絡みます。

- 文字列は`"`で囲む
- 数字・`boolean`などは`"`で囲まない
- オブジェクトはその型ごとにシリアライズ方法を取る必要が有る
  - e.g. `JsonValue`アノテーションが設定されている場合

今回のサンプルで取り上げている`AbstractValue`は`Comparable`なら何でも設定可能なため、これらのロジックを考慮する必要が有ります。
`defaultSerializeValue`を使うことで、これらはよしなに取り扱われます。

## シリアライザーの登録
シリアライザーの登録は以下のように行います。

```java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wrongwrong.values.AbstractValue;

/* classなどは省略 */

SimpleModule module = new SimpleModule();

JsonSerializer<AbstractValue<?>> serializer = new AbstractValueSerializer();
module.addSerializer(serializer);

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
```

実際の環境では、恐らく1インスタンスの`ObjectMapper`が使い回されているため、そこにコンフィグ等で`Module`を登録する形になると思います。
`Spring`では、`WebMvcConfig`辺りで設定することができます。

# 動かしてみる
最後に、実際に動かした様子を紹介します。
型ごとに適切なシリアライズが行われていることが分かります。

**シリアライズ対象のフィールドとなるクラス**

```java:中身はint
package com.wrongwrong.values;

public class Foo extends AbstractValue<Integer> {
    private final int value;

    public Foo(int value) { this.value = value; }

    @Override
    public Integer getValue() { return value; }
}
```

```java:中身はString
public class Bar extends AbstractValue<String> {
    private final String value;

    public Bar(String value) { this.value = value; }

    @Override
    public String getValue() { return value; }
}
```

```java:中身はObject
package com.wrongwrong.values;

import lombok.Data;

public class Baz extends AbstractValue<Baz.InnerObject> {
    @Data
    public static class InnerObject implements Comparable<InnerObject> {
        private final int f1;
        private final String f2;

        public InnerObject(int f1, String f2) {
            this.f1 = f1;
            this.f2 = f2;
        }

        @Override
        public int compareTo(InnerObject o) {
            int temp = Integer.compare(f1, o.f1);
            return temp == 0 ? f2.compareTo(o.f2) : temp;
        }
    }

    private final InnerObject value;

    public Baz(InnerObject value) { this.value = value; }

    @Override
    public InnerObject getValue() { return value; }
}
```

**シリアライズ対象クラス**

```java
package com.wrongwrong;

import com.wrongwrong.values.Bar;
import com.wrongwrong.values.Baz;
import com.wrongwrong.values.Foo;
import lombok.Data;

@Data
public class Pojo {
    private Foo foo;
    private Bar bar;
    private Baz baz;

    public static Pojo generateSample() {
        Pojo pojo = new Pojo();
        pojo.foo = new Foo(5);
        pojo.bar = new Bar("bar");
        pojo.baz = new Baz(new Baz.InnerObject(10, "f2"));

        return pojo;
    }
}
```

**テストコード**

```java
package com.wrongwrong.ser;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wrongwrong.Pojo;
import com.wrongwrong.values.AbstractValue;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class SerializerTest {
    @Test
    void test() throws JsonProcessingException {
        SimpleModule module = new SimpleModule();

        JsonSerializer<AbstractValue<?>> serializer = new AbstractValueSerializer();
        module.addSerializer(serializer);

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(module);

        // -> {"foo":5,"bar":"bar","baz":{"f1":10,"f2":"f2"}}
        assertEquals(
                "{\"foo\":5,\"bar\":\"bar\",\"baz\":{\"f1\":10,\"f2\":\"f2\"}}",
                mapper.writeValueAsString(Pojo.generateSample())
        );
    }
}
```