1. 序章
すべてのフィールドの正確な1対1の表現を使用して、完全なデータ構造をJSONにシリアル化することは、適切でない場合や、単に必要なものではない場合があります。 その代わり、 データの拡張ビューまたは簡略化ビューを作成したい場合があります。 ここで、カスタムジャクソンシリアライザーが登場します。
ただし、カスタムシリアライザーの実装は、特にモデルオブジェクトに多数のフィールド、コレクション、またはネストされたオブジェクトがある場合、面倒な場合があります。 幸い、 Jacksonライブラリには、この作業を非常に簡単にすることができるいくつかのプロビジョニングがあります。
この短いチュートリアルでは、カスタムJacksonシリアライザーを見て、カスタムシリアライザー内のデフォルトシリアライザーにアクセスする方法を示します。
2. サンプルデータモデル
Jacksonのカスタマイズに飛び込む前に、シリアル化するサンプルFolderクラスを見てみましょう。
public class Folder {
private Long id;
private String name;
private String owner;
private Date created;
private Date modified;
private Date lastAccess;
private List<File> files = new ArrayList<>();
// standard getters and setters
}
そして、 File クラスは、Folderクラス内のListとして定義されています。
public class File {
private Long id;
private String name;
// standard getters and setters
}
3. ジャクソンのカスタムシリアライザー
カスタムシリアライザーを使用する主な利点は、クラス構造を変更する必要がないことです。 さらに、予想される動作をクラス自体から簡単に切り離すことができます。
したがって、Folderクラスの縮小表示が必要だと想像してみましょう。
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
]
}
次のセクションで説明するように、ジャクソンで目的の出力を達成する方法はいくつかあります。
3.1. ブルートフォースアプローチ
まず、Jacksonのデフォルトのシリアライザーを使用せずに、すべての面倒な作業を自分で行うカスタムシリアライザーを作成できます。
これを実現するために、Folderクラスのカスタムシリアライザーを作成しましょう。
public class FolderJsonSerializer extends StdSerializer<Folder> {
public FolderJsonSerializer() {
super(Folder.class);
}
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
gen.writeArrayFieldStart("files");
for (File file : value.getFiles()) {
gen.writeStartObject();
gen.writeNumberField("id", file.getId());
gen.writeStringField("name", file.getName());
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}
したがって、 Folder クラスを、必要なフィールドのみを含む縮小ビューにシリアル化できます。
3.2. 内部ObjectMapperの使用
カスタムシリアライザーは、すべてのプロパティを詳細に変更する柔軟性を提供しますが、 Jacksonのデフォルトのシリアライザーを再利用することで、作業を簡単にすることができます。
デフォルトのシリアライザーを使用する1つの方法は、内部ObjectMapperクラスにアクセスすることです。
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
ObjectMapper mapper = (ObjectMapper) gen.getCodec();
gen.writeFieldName("files");
String stringValue = mapper.writeValueAsString(value.getFiles());
gen.writeRawValue(stringValue);
gen.writeEndObject();
}
したがって、Jacksonは、FileオブジェクトのListをシリアル化することで手間のかかる作業を処理するだけで、出力は同じになります。
3.3. SerializerProviderを使用する
デフォルトのシリアライザーを呼び出す別の方法は、 SerializerProvider。を使用することです。したがって、プロセスをFileタイプのデフォルトのシリアライザーに委任します。
それでは、 SerializerProvider を使用して、コードを少し単純化してみましょう。
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeEndObject();
}
そして、前と同じように、同じ出力が得られます。
4. 考えられる再帰の問題
ユースケースによっては、Folderの詳細を含めることでシリアル化されたデータを拡張する必要がある場合があります。 これは、レガシーシステムまたは外部アプリケーションが統合され、変更する機会がないためである可能性があります。
シリアライザーを変更して、シリアル化されたデータの details フィールドを作成し、Folderクラスのすべてのフィールドを公開します。
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
// this line causes exception
provider.defaultSerializeField("details", value, gen);
gen.writeEndObject();
}
今回は、StackOverflowError例外が発生します。
カスタムシリアライザーを定義すると、Jacksonは、タイプFolder用に作成された元のBeanSerializerインスタンスを内部的にオーバーライドします。 その結果、 SerializerProvider は、デフォルトのシリアライザーではなく、カスタマイズされたシリアライザーを毎回検出します。これにより、無限ループが発生します。
では、この問題をどのように解決するのでしょうか。 次のセクションでは、このシナリオに使用できる1つのソリューションを紹介します。
5. BeanSerializerModifierを使用する
考えられる回避策は、 BeanSerializerModifier を使用して、タイプ Folder のデフォルトのシリアライザーを、Jacksonが内部でオーバーライドする前に保存することです。
シリアライザーを変更して、フィールドを追加しましょう— defaultSerializer :
private final JsonSerializer<Object> defaultSerializer;
public FolderJsonSerializer(JsonSerializer<Object> defaultSerializer) {
super(Folder.class);
this.defaultSerializer = defaultSerializer;
}
次に、 BeanSerializerModifier の実装を作成して、デフォルトのシリアライザーを渡します。
public class FolderBeanSerializerModifier extends BeanSerializerModifier {
@Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(Folder.class)) {
return new FolderJsonSerializer((JsonSerializer<Object>) serializer);
}
return serializer;
}
}
次に、 BeanSerializerModifier をモジュールとして登録して、機能させる必要があります。
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FolderBeanSerializerModifier());
mapper.registerModule(module);
次に、detailsフィールドにdefaultSerializerを使用します。
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeFieldName("details");
defaultSerializer.serialize(value, gen, provider);
gen.writeEndObject();
}
最後に、filesフィールドをdetailsから削除することもできます。これは、すでにシリアル化されたデータに個別に書き込んでいるためです。
したがって、Folderクラスのfilesフィールドは無視します。
@JsonIgnore
private List<File> files = new ArrayList<>();
最後に、問題が解決され、期待される出力も得られます。
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
],
"details": {
"id":1,
"name": "Root Folder",
"owner": "root",
"created": 1565203657164,
"modified": 1565203657164,
"lastAccess": 1565203657164
}
}
6. 結論
このチュートリアルでは、JacksonLibraryのカスタムシリアライザー内でデフォルトのシリアライザーを呼び出す方法を学びました。
いつものように、このチュートリアルで使用されているすべてのコード例は、GitHubでから入手できます。