TL;DR
- 当該の抽象型向けの
StdSerializer
を定義し、適切に登録することで実現できる - 中身の型ごとのシリアライズは、
SerializerProvider::findValueSerializer
で取得したシリアライザーで行える
やること
以下のような、値をラップする抽象型が有るとします。
用途としては、DDD
でいう値オブジェクトを定義する時、共通の振る舞いを持たせるために基底となる型を作る場合を想定しています。
package com.wrongwrong.values;
public abstract class AbstractValue<T extends Comparable<T>> {
public abstract T getValue();
}
このクラスは、以下のように継承されて使われます。
package com.wrongwrong.values;
public class Foo extends AbstractValue<Integer> {
private final int value;
public Foo(int value) { this.value = value; }
@Override
public Integer getValue() { return value; }
}
この型をfoo
というフィールドに設定した場合、そのままJSON
にシリアライズした結果は以下のようになります。
{
"foo": {
"value": 10
}
}
一方、この型はあくまでサーバーサイドの都合でラップするためのものであるため、フロントへ渡すシリアライズ結果は以下のようにunbox
された形になっている方が望ましいです。
{
"foo": 10
}
このunbox
するシリアライズ処理は、個別に書くと大変で、かといってAbstractValue
側にJsonValue
アノテーションを設定するのも不適切です1。
そこで、Jackson
を用いて共通のunbox
するシリアライズ処理を実装します。
やり方
ここではAbstractValue
をシリアライズするStdSerializer
を実装し、SimpleModule
を介してObjectMapper
に登録する方法を紹介します。
シリアライザーの実装
シリアライザーの実装は以下の通りです。
package com.wrongwrong.ser;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.wrongwrong.values.AbstractValue;
import java.io.IOException;
public class AbstractValueSerializer extends StdSerializer<AbstractValue<?>> {
public AbstractValueSerializer() {
// AbstractValueがジェネリクスを持つ関係で、dummyフラグを設定する必要が有る
super(AbstractValue.class, true);
}
@Override
public void serialize(AbstractValue<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Object unboxed = value.getValue();
if (unboxed == null) {
gen.writeNull();
} else {
// findValueSerializerは必ず何かしらのシリアライザーを返す
JsonSerializer<Object> serializer = provider.findValueSerializer(unboxed.getClass());
serializer.serialize(unboxed, gen, provider);
}
}
}
注意点
Java
上の値からJSON
へシリアライズする時には、以下のように様々なロジックが絡みます。
- 文字列は
"
で囲む - 数字・
boolean
などは"
で囲まない - オブジェクトはその型ごとにシリアライズ方法を取る必要が有る
- e.g.
JsonValue
アノテーションが設定されている場合
- e.g.
今回のサンプルで取り上げているAbstractValue
はComparable
なら何でも設定可能なため、これらのロジックを考慮する必要が有ります。
これを行っているのが以下の2行です。
JsonSerializer<Object> serializer = provider.findValueSerializer(unboxed.getClass());
serializer.serialize(unboxed, gen, provider);
シリアライザーの登録
シリアライザーの登録は以下のように行います。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wrongwrong.values.AbstractValue;
/* classなどは省略 */
SimpleModule module = new SimpleModule();
JsonSerializer<AbstractValue<?>> serializer = new AbstractValueSerializer();
module.addSerializer(serializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
実際の環境では、恐らく1インスタンスのObjectMapper
が使い回されているため、そこにコンフィグ等でModule
を登録する形になると思います。
Spring
では、WebMvcConfig
辺りで設定することができます。
動かしてみる
最後に、実際に動かした様子を紹介します。
型ごとに適切なシリアライズが行われていることが分かります。
シリアライズ対象のフィールドとなるクラス
package com.wrongwrong.values;
public class Foo extends AbstractValue<Integer> {
private final int value;
public Foo(int value) { this.value = value; }
@Override
public Integer getValue() { return value; }
}
public class Bar extends AbstractValue<String> {
private final String value;
public Bar(String value) { this.value = value; }
@Override
public String getValue() { return value; }
}
package com.wrongwrong.values;
import lombok.Data;
public class Baz extends AbstractValue<Baz.InnerObject> {
@Data
public static class InnerObject implements Comparable<InnerObject> {
private final int f1;
private final String f2;
public InnerObject(int f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
@Override
public int compareTo(InnerObject o) {
int temp = Integer.compare(f1, o.f1);
return temp == 0 ? f2.compareTo(o.f2) : temp;
}
}
private final InnerObject value;
public Baz(InnerObject value) { this.value = value; }
@Override
public InnerObject getValue() { return value; }
}
シリアライズ対象クラス
package com.wrongwrong;
import com.wrongwrong.values.Bar;
import com.wrongwrong.values.Baz;
import com.wrongwrong.values.Foo;
import lombok.Data;
@Data
public class Pojo {
private Foo foo;
private Bar bar;
private Baz baz;
public static Pojo generateSample() {
Pojo pojo = new Pojo();
pojo.foo = new Foo(5);
pojo.bar = new Bar("bar");
pojo.baz = new Baz(new Baz.InnerObject(10, "f2"));
return pojo;
}
}
テストコード
package com.wrongwrong.ser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.wrongwrong.Pojo;
import com.wrongwrong.values.AbstractValue;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SerializerTest {
@Test
void test() throws JsonProcessingException {
SimpleModule module = new SimpleModule();
JsonSerializer<AbstractValue<?>> serializer = new AbstractValueSerializer();
module.addSerializer(serializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
// -> {"foo":5,"bar":"bar","baz":{"f1":10,"f2":"f2"}}
assertEquals(
"{\"foo\":5,\"bar\":\"bar\",\"baz\":{\"f1\":10,\"f2\":\"f2\"}}",
mapper.writeValueAsString(Pojo.generateSample())
);
}
}
-
AbstractValue
に表示用ロジックが入ってしまうのは責務が漏れていると言えるため。 ↩