【Jackson】nullを特定の値にシリアライズする3通りの方法

## TL;DR
`Jackson`で`null`を特定の値にシリアライズする場合、以下の3つの方法が有ります[^haaku]。

[^haaku]: 自分が把握している限りです。

1. `ObjectMapper`にシリアライザーを設定する
2. `JsonSerialize`アノテーションにシリアライザーを指定する
3. マッパー固有の方法で設定する

以下、それぞれの方法について書きます。
ここで、説明のためのシリアライザーとしては以下を用います。
このシリアライザーは`null`を`n/a`という文字列にシリアライズします。

```java
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

public class MyNullValueSerializer extends JsonSerializer<Object> {
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) throw new RuntimeException("このシリアライザーはnull以外の値を処理できません");

        gen.writeString("n/a");
    }
}
```

また、この記事では`Java 16`/`Jackson 2.13.4`を利用しています。

## `ObjectMapper`にシリアライザーを設定する
まず、`ObjectMapper.getSerializerProvider().setNullValueSerializer`で、`ObjectMapper`にシリアライザーを設定する方法です。
この方法は特に`null`だったプロパティ全てを同じように取り扱いたい場合に有効です。

```java:ObjectMapperにシリアライザーを設定するサンプル
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ToObjectMapper {
    private static final ObjectMapper mapper;

    static {
        mapper = new ObjectMapper();
        mapper.getSerializerProvider().setNullValueSerializer(new MyNullValueSerializer());
    }

    public static void main(String[] args) throws JsonProcessingException {
        Src src = new Src(null, "bar");
        System.out.println(mapper.writeValueAsString(src));
    }
}
```

```json:実行結果
{"foo":"n/a","bar":"bar"}
```

シリアライズ対象クラスには以下のようなクラスを用いる想定です[^record]。

[^record]: 断りなく`record class`を使っていますが、説明の大筋には関係無いので言及はしません。

```java
public record Src(String foo, String bar) {}
```

### 対象を特定の型に限定したい場合
上記の方法は、`null`だった際の扱いが全ての型に適用されてしまうという欠点が有ります。
適用する型を絞りたい場合、適切に実装した`AnnotationIntrospector`を登録する方法が有ります。

以下は対象となる型`String`に絞った`AnnotationIntrospector`です。

```java
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;

public class MyAnnotationIntrospector extends NopAnnotationIntrospector {
    @Override
    public Object findNullSerializer(Annotated am) {
        if (am.getRawType() == String.class) {
            return new MyNullValueSerializer();
        }

        return null;
    }
}
```

登録方法は以下の通りです。

```diff_java
    static {
        mapper = new ObjectMapper();
+       mapper.setAnnotationIntrospector(new MyAnnotationIntrospector());
-       mapper.getSerializerProvider().setNullValueSerializer(new MyNullValueSerializer());
    }
```

## JsonSerializeアノテーションにシリアライザーを指定する
次に、`JsonSerialize`アノテーションの`nullsUsing`にシリアライザーを指定する方法です。
この方法は特定プロパティが`null`だった場合の挙動を設定する際に有効ですが、一々アノテーションを設定する必要が有るため、`null`全てを同じように取り扱いたい場合には向きません。

```java:JsonSerializeアノテーションにシリアライザーを指定するサンプル
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

public record Src(@JsonSerialize(nullsUsing = MyNullValueSerializer.class) String foo, String bar) {}
```

```java:呼び出しサンプル
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ToAnnotation {
    private static final ObjectMapper mapper = new ObjectMapper();

    public static void main(String[] args) throws JsonProcessingException {
        Src src = new Src(null, null);
        System.out.println(mapper.writeValueAsString(src));
    }
}
```

実行結果からは、`JsonSerialize`アノテーションの設定の有無でシリアライズ結果が異なっていることが分かります。

```json:実行結果
{"foo":"n/a","bar":null}
```

## マッパー固有の方法で設定する
`Jackson`では、`jackson-dataformats-text`として様々なフォーマットのファイルに関する取り扱いがサポートされており、これらの中には`null`を特定の値にシリアライズする独自の方法が提供されているものが有ります。

https://github.com/FasterXML/jackson-dataformats-text

例えば、`jackson-dataformat-csv`のシリアライズに用いられる`CsvSchema`は、`Builder`から初期化する場合、`null`時に出力する値を設定することができます。

https://javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-csv/latest/com/fasterxml/jackson/dataformat/csv/CsvSchema.Builder.html#setNullValue-char:A-