Jackson におけるカスタムアノテーションによるフィールドのシリアライズ制御

この機能は高度かつ実用的なものです。Spring Boot 環境において、Jackson の ContextualSerializer および ContextualDeserializer インターフェースと組み合わせて、特定のフィールドのシリアライズ/デシリアライズロジックを細かく制御できます。 このアプローチの中心は以下の通りです:
  • アノテーション:設定値の格納先
  • シリアライザ:実際の変換処理
  • Contextual インターフェース:アノテーションとシリアライザの橋渡し
以下では、JSON配列をカンマ区切り文字列に変換する機能を実装する例を示します。

目的

ある User クラスを考えます。このクラスの hobbies フィールドは Java 上では List<String> ですが、データベース上ではカンマ区切りの文字列(例:"reading,sports")として保存されているとします。 この場合、以下の要件を満たすようにします:
  1. シリアライズ(Java → JSON):リストをカンマ区切り文字列へ変換
  2. デシリアライズ(JSON → Java):カンマ区切り文字列をリストへ変換
  3. 柔軟性:カスタムアノテーション @StringListConverter を用いて区切り文字を指定(デフォルトはカンマ)

ステップ1:カスタムアノテーションの定義

アノテーションには @JacksonAnnotationsInside を付与する必要があります。これにより Jackson がアノテーションを認識し、適切に処理できるようになります。
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = CustomStringListSerializer.class)
@JsonDeserialize(using = CustomStringListDeserializer.class)
public @interface StringListConverter {
    String delimiter() default ",";
}

ステップ2:シリアライザの実装

シリアライザは ContextualSerializer インターフェースを実装する必要があります。これにより、フィールドのアノテーションから区切り文字を取得できます。
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

public class CustomStringListSerializer extends JsonSerializer<List<String>> implements ContextualSerializer {

    private String delimiter;

    public CustomStringListSerializer() {
        this.delimiter = ",";
    }

    public CustomStringListSerializer(String delimiter) {
        this.delimiter = delimiter;
    }

    @Override
    public void serialize(List<String> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        String result = value.stream().collect(Collectors.joining(delimiter));
        gen.writeString(result);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        StringListConverter annotation = property.getAnnotation(StringListConverter.class);
        if (annotation != null) {
            return new CustomStringListSerializer(annotation.delimiter());
        }
        return prov.findNullValueSerializer(property);
    }
}

ステップ3:デシリアライザの実装

デシリアライザも ContextualDeserializer を実装します。
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class CustomStringListDeserializer extends JsonDeserializer<List<String>> implements ContextualDeserializer {

    private String delimiter;

    public CustomStringListDeserializer() {
        this.delimiter = ",";
    }

    public CustomStringListDeserializer(String delimiter) {
        this.delimiter = delimiter;
    }

    @Override
    public List<String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String text = p.getText();
        if (text == null || text.isEmpty()) {
            return Collections.emptyList();
        }
        return Arrays.asList(text.split(delimiter));
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        StringListConverter annotation = property.getAnnotation(StringListConverter.class);
        if (annotation != null) {
            return new CustomStringListDeserializer(annotation.delimiter());
        }
        return ctxt.findNullValueDeserializer(property);
    }
}

ステップ4:エンティティクラスでの利用

次のように、定義したアノテーションをフィールドに適用します。
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class User {
    private String name;

    @StringListConverter
    private List<String> hobbies;

    @StringListConverter(delimiter = "|")
    private List<String> tags;
}

ステップ5:動作確認テスト

以下のようなテストコードで動作を確認できます。
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;

public class TestJackson {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User();
        user.setName("張三");
        user.setHobbies(Arrays.asList("閱讀", "游泳"));
        user.setTags(Arrays.asList("Java", "Spring"));

        String json = mapper.writeValueAsString(user);
        System.out.println("シリアライズ結果: " + json);

        String inputJson = "{\"name\":\"李四\",\"hobbies\":\"篮球,足球\",\"tags\":\"前端|Vue\"}";
        User deserializedUser = mapper.readValue(inputJson, User.class);
        System.out.println("デシリアライズ結果: " + deserializedUser);
    }
}

重要なポイント

  1. @JacksonAnnotationsInside:これはカスタムアノテーションの必須要素であり、Jackson がアノテーションを無視しないようにします。
  2. ContextualSerializer/ContextualDeserializer:このインターフェースにより、アノテーションのパラメータ(例:delimiter)に基づいて動的にシリアライザを生成できます。
  3. property.getAnnotation(...)createContextual メソッド内で、フィールドのアノテーション情報を取得して設定値を読み込みます。

タグ: Jackson Java JSON アノテーション シリアライズ

7月16日 17:37 投稿