JacksonによるJSONシリアライズ時のグローバルフィールドインターセプション

Spring Bootアプリケーションにおいて、Jacksonを用いたオブジェクトからJSONへの変換処理中に、任意のロジックを挿入してフィールド値を動的に加工する方法について解説します。代表的なユースケースには、機密情報のマスキング(例:電話番号や身分証番号)、辞書値の変換(例:1→「男」)、null値の空文字置換などが含まれます。

1. BeanSerializerModifierによるオブジェクトレベルのグローバルインターセプション

この手法は、すべてのBeanオブジェクトのシリアライズ処理を一元的にフックできるため、アノテーション不要で柔軟な制御が可能です。

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.BeanSerializer;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.lang.reflect.Field;

@Configuration
public class GlobalSerializationConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SerializerFactory factory = mapper.getSerializerFactory()
                .withSerializerModifier(new CustomBeanSerializerModifier());
        mapper.setSerializerFactory(factory);
        return mapper;
    }

    static class CustomBeanSerializerModifier extends BeanSerializerModifier {
        @Override
        public JsonSerializer<?> modifySerializer(
                SerializationConfig config,
                BeanDescription desc,
                JsonSerializer<?> serializer) {
            if (serializer instanceof BeanSerializer) {
                return new MaskingFieldSerializer((BeanSerializer) serializer);
            }
            return serializer;
        }
    }

    static class MaskingFieldSerializer extends StdSerializer<Object> {
        private final BeanSerializer original;

        protected MaskingFieldSerializer(BeanSerializer src) {
            super(Object.class);
            this.original = src;
        }

        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider provider)
                throws IOException {
            gen.writeStartObject();

            Class<?> clazz = value.getClass();
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                try {
                    String name = field.getName();
                    Object val = field.get(value);

                    if (val == null) {
                        gen.writeStringField(name, "");
                        continue;
                    }

                    String strVal = val.toString();
                    if (name.contains("phone") && strVal.length() == 11) {
                        gen.writeStringField(name, maskPhone(strVal));
                        continue;
                    }

                    if (name.contains("idCard") || name.contains("identity")) {
                        gen.writeStringField(name, maskIdCard(strVal));
                        continue;
                    }

                    gen.writeObjectField(name, val);
                } catch (IllegalAccessException ignored) {
                }
            }

            gen.writeEndObject();
        }

        private String maskPhone(String phone) {
            return phone.substring(0, 3) + "****" + phone.substring(7);
        }

        private String maskIdCard(String id) {
            return id.length() >= 18 ?
                id.substring(0, 6) + "********" + id.substring(14) : id;
        }
    }
}

2. ContextualSerializerによるフィールド単位の動的処理

特定のフィールドにカスタムロジックを適用したい場合に使用します。フィールド名やアノテーション情報を取得でき、柔軟な条件分岐が可能です。

public class SensitiveDataSerializer extends JsonSerializer<String>
        implements ContextualSerializer {

    private String fieldName;

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty prop) {
        this.fieldName = prop.getName();
        return this;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        if (value == null) {
            gen.writeString("");
            return;
        }

        if (fieldName != null && fieldName.contains("email")) {
            gen.writeString(maskEmail(value));
        } else {
            gen.writeString(value);
        }
    }

    private String maskEmail(String email) {
        int atIndex = email.indexOf('@');
        if (atIndex <= 0) return email;
        String prefix = email.substring(0, 1) + "***";
        return prefix + email.substring(atIndex);
    }
}

3. 型ベースのグローバルシリアライザ

すべてのString型やInteger型など、特定のデータ型に対して共通処理を適用できます。

@Bean
public SimpleModule stringHandlingModule() {
    SimpleModule module = new SimpleModule();
    module.addSerializer(String.class, new JsonSerializer<String>() {
        @Override
        public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
                throws IOException {
            gen.writeString(value == null ? "" : value.trim());
        }
    });
    return module;
}

4. アノテーション駆動型のインターセプション

AnnotationIntrospectorを拡張することで、独自アノテーションに基づいて自動的にシリアライザを適用できます。ただし実装が複雑なため、特殊な要件に限定して使用します。

比較と推奨用途

方式 適用スコープ フィールド/アノテーション情報取得 実装難易度 推奨度
BeanSerializerModifier 全オブジェクト 可(リフレクション経由) ★★★★★
ContextualSerializer 個別フィールド 可(直接取得) ★★★★★
JsonSerializer(型登録) データ型全体 不可 ★★★☆☆
AnnotationIntrospector アノテーション対象 ★★★☆☆

一般的な要件では、BeanSerializerModifierによるグローバルフィールド処理と、ContextualSerializerによるフィールド単位の動的制御を組み合わせることで、ほとんどのシリアライズカスタマイズに対応可能です。

タグ: Jackson Spring Boot JSONシリアライズ フィールドマスキング BeanSerializerModifier

7月21日 16:46 投稿