Jacksonのシリアライザーの「三つの主要コンポーネント」について解説します。これらのコンポーネントはそれぞれ異なるレベルで問題を解決し、Jacksonの理解を深めるための重要な要素です。
JsonSerializer:具体的な変換ロジックを定義する。ContextualSerializer:注釈やフィールド情報に基づいて動的にロジックを調整する。BeanSerializerModifier:シリアライズ開始前に属性を一括で修正したり、特定のシリアライザーを指定したりする。
以下、各コンポーネントの詳細とコード例を示します。
1. JsonSerializer:基本的な実行者
これは最も基本的なシリアライザインターフェースです。Javaオブジェクトや特定のタイプをJSONに変換するカスタムロジックが必要な場合、このクラスを継承します。
- 役割:具体的なシリアライズ動作を定義する。
- 特徴:インスタンス化後、その動作は通常固定され、現在のフィールドに注釈があるかどうか、フィールド名を認識することはできません。
- 適用場面:グローバルに通用する型変換(例:
DateをStringに変換)または@JsonSerialize(using=...)で明示的に指定されたフィールド。
コード例:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
// すべての文字列を大文字に変換するシンプルなシリアライザー
public class UpperCaseSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value != null) {
gen.writeString(value.toUpperCase());
} else {
gen.writeNull();
}
}
}
2. ContextualSerializer:文脈感知者
JsonSerializer はどのフィールドで使用されているかを知ることができません。ContextualSerializer インターフェースを実装したシリアライザーは、シリアライズが発生する前にフィールドの文脈情報を取得できます(例えば、フィールド上の注釈、フィールド名、フィールドタイプ)。
- 役割:文脈(注釈、フィールドメタデータ)に基づいてシリアライザインスタンスを動的に生成または設定する。
- 主要なメソッド:
createContextual(SerializerProvider, BeanProperty)。 - 動作フロー:
- Jackson は特定のフィールドをシリアライズする必要があることを検出する。
- そのフィールドのシリアライザーが
ContextualSerializerを実装している場合、Jackson は最初にcreateContextualを呼び出す。 - このメソッド内でフィールド上の注釈(例:
@MyAnnotation)を読み取ることができます。 - 注釈の値に基づいて、新しい構成済みの
JsonSerializerインスタンス(通常は自分自身)を返す。 - 最後に
serializeメソッドを呼び出す。
- 適用場面:カスタム注釈によるロジック(例:データマスキング、辞書変換)。
コード例(データマスキングの実装):
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.Objects;
// 1. マスキング注釈の定義
// @Retention(RetentionPolicy.RUNTIME)
// @JacksonAnnotationsInside
// @JsonSerialize(using = DesensitizationSerializer.class) // キー:シリアライザーを指定
// public @interface Desensitization { ... }
// 2. ContextualSerializer の実装
public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer {
private boolean isMobile; // 注釈から取得した設定を保存
// デフォルトコンストラクター(Jackson が必要)
public DesensitizationSerializer() {
this.isMobile = false;
}
// 引数付きコンストラクター(構成済みインスタンスを返す)
public DesensitizationSerializer(boolean isMobile) {
this.isMobile = isMobile;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
// フィールド上の注釈を取得
Desensitization annotation = property.getAnnotation(Desensitization.class);
if (annotation != null) {
// 注釈のプロパティに基づいて動作を決定
boolean mobile = true; // 例:注釈から電話番号であることが判明
return new DesensitizationSerializer(mobile);
}
return prov.findNullValueSerializer(property); // 注釈がない場合はデフォルトを返す
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// createContextual で設定された isMobile 変数を使用
if (isMobile && value != null && value.length() == 11) {
gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
} else {
gen.writeString(value);
}
}
}
3. BeanSerializerModifier:全体の指揮官
これは抽象クラスであり、Jacksonがシリアライザーを構築する過程で介入できるようにします。個々の値の変換ではなく、全体のBeanの属性リストに対して操作を行います。
- 役割:シリアライズ開始前にBeanの属性リスト(
List<BeanPropertyWriter>)を修正する、または特定の属性のシリアライザーを置き換える。 - 主要なメソッド:
changeProperties(...)。 - 特徴:グローバルに効果があります。
ObjectMapperに登録すると、そのMapperを経由してシリアライズされるすべてのBeanがチェックされます。 - 適用場面:
- グローバルなnull処理:すべての
null値を""または0に統一する。 - グローバルなLong to String:フロントエンドでの精度損失を解決する。
- 動的なフィールド追加:返されるJSONに
versionフィールドをハードコーディングで追加する。 - フィールドフィルタリング:権限に基づいて特定のフィールドを動的に削除する。
コード例(グローバルなLong to String):
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.util.List;
public class GlobalModifier extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter writer : beanProperties) {
// 属性タイプを確認
if (writer.getType().getRawClass().equals(Long.class)) {
// シリアライザーをToStringSerializerに強制設定
writer.assignSerializer(ToStringSerializer.instance);
}
}
return beanProperties;
}
}
まとめと比較
以下の比較表を用いて、各コンポーネントの違いをより明確に理解することができます:
| 特性 | JsonSerializer | ContextualSerializer | BeanSerializerModifier |
|---|---|---|---|
| **核心的責任** | **実行**:Java値をJSONに書き込む。 | **構成**:文脈(注釈)に基づいてJsonSerializerを生成/設定する。 | **修正**:シリアライズ前にBeanの属性構造を修正またはシリアライザーを指定する。 |
| **認識能力** | **盲**:フィールド名や注釈を知らない。 | **強**:フィールド名、タイプ、注釈情報を知っている。 | **広**:Beanのすべての属性リストを知っている。 |
| **効果範囲** | 局所的(通常 @JsonSerialize と組み合わせて使用)。 |
局所的(通常カスタム注釈と組み合わせて使用)。 | **グローバル**(ObjectMapperに登録後、すべてのクラスに効果がある)。 |
| **典型的な用途** | 日付フォーマット、オブジェクトを文字列に変換。 | データマスキング(注釈タイプに基づく)、辞書変換。 | グローバルなLong to String、グローバルなnull値処理、追加フィールドの挿入。 |
| **コード複雑度** | ⭐ | ⭐⭐⭐ | ⭐⭐ |
ベストプラクティスの提案:
- 単純な型変換の場合、直接
JsonSerializerを記述する。 - 注釈に基づく動的な変更(例:「電話番号のマスキング」と「ID番号のマスキング」が同じロジックだが異なるパラメータを持つ場合)には
ContextualSerializerを使用する。 - プロジェクト全体の統一規則(例:「すべてのLongをStringに変換」)には
BeanSerializerModifierを使用する。