この機能は高度かつ実用的なものです。Spring Boot 環境において、Jackson の
ContextualSerializer および ContextualDeserializer インターフェースと組み合わせて、特定のフィールドのシリアライズ/デシリアライズロジックを細かく制御できます。
このアプローチの中心は以下の通りです:
- アノテーション:設定値の格納先
- シリアライザ:実際の変換処理
Contextualインターフェース:アノテーションとシリアライザの橋渡し
目的
あるUser クラスを考えます。このクラスの hobbies フィールドは Java 上では List<String> ですが、データベース上ではカンマ区切りの文字列(例:"reading,sports")として保存されているとします。
この場合、以下の要件を満たすようにします:
- シリアライズ(Java → JSON):リストをカンマ区切り文字列へ変換
- デシリアライズ(JSON → Java):カンマ区切り文字列をリストへ変換
- 柔軟性:カスタムアノテーション
@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);
}
}
重要なポイント
@JacksonAnnotationsInside:これはカスタムアノテーションの必須要素であり、Jackson がアノテーションを無視しないようにします。ContextualSerializer/ContextualDeserializer:このインターフェースにより、アノテーションのパラメータ(例:delimiter)に基づいて動的にシリアライザを生成できます。property.getAnnotation(...):createContextualメソッド内で、フィールドのアノテーション情報を取得して設定値を読み込みます。