Jackson でオブジェクトのシリアライザ/デシリアライザを作ってみる
Jacksonでオブジェクトのシリアライザを作成してみます。
通常の ObjectMapper の設定のままだと getter に対する setter が必要だったり、ジェネリクスが扱えなかったりと色々条件があるので、ObjectMapper に次の設定を施して実施します。
- 全フィールド値のみ対象とする(getter/setter は対象としない)
- ジェネリクスに対応する為、自身の型を埋め込む
- @JsonIgnoreが付いていてもシリアライズ対象にする
- null は出力しない(データ量削減)
ObjectMapper 側で色々設定する事で、オブジェクト側は特に実装の変更を必要としない点がミソです。
シリアライザ/デシリアライザソース
public class JacksonSerializer {
private static final ObjectMapper mapper = new ObjectMapper();
static {
// Fieldのみ対象に設定
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
// Generics 対応
StdTypeResolverBuilder typeResolverBuilder =
new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, null);
typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
typeResolverBuilder.typeProperty("classType");
mapper.setDefaultTyping(typeResolverBuilder);
//@JsonIgnore を無視
AnnotationIntrospector introspector = new JsonIgnoreAnnotationIgnoreInterceptor();
mapper.setAnnotationIntrospector(introspector);
// Null を出力しない
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// @JsonSerialize(include=Include.NON_NULL) と同じ設定
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
private static class JsonIgnoreAnnotationIgnoreInterceptor
extends JacksonAnnotationIntrospector {
/** */
private static final long serialVersionUID = 1L;
/**
* フィールドは @JsonIgnore が無い事、メソッドは @JsonIgnore がある事にする
**/
@Override
public boolean _isIgnorable(Annotated a) {
return a instanceof AnnotatedMethod;
}
/**
* メソッド時は @JsonProperty や @JsonView などがあっても null を返す
*/
@Override
public PropertyName findNameForSerialization(Annotated a) {
return a instanceof AnnotatedMethod ?
null : super.findNameForSerialization(a);
}
/**
* メソッド時は @JsonProperty や @JsonView などがあっても null を返す
*/
@Override
public PropertyName findNameForDeserialization(Annotated a) {
return a instanceof AnnotatedMethod ?
null : super.findNameForDeserialization(a);
}
}
public static byte[] serialize(Object source) throws Throwable {
return mapper.writeValueAsBytes(source);
}
public static String serializeAsString(Object source) throws Throwable {
return mapper.writeValueAsString(source);
}
public static <T> T deserialize(String json, Class<T> type) throws Throwable {
return mapper.readValue(json, type);
}
public static <T> T deserialize(byte[] data, Class<T> type) throws Throwable {
return mapper.readValue(data, type);
}
}
テストコード
public void main() throws Throwable {
SubClass sub = new SubClass();
sub.id = 1234;
sub.name = "aiueo";
DerivedClass derived = new DerivedClass();
derived.id = 5678;
derived.number = 1;
Array hoge = new Array();
hoge.list.add(sub);
hoge.list.add(derived);
String json = JacksonSerializer.serializeAsString(hoge);
System.out.println(json);
// deserialize が成功するか確認
Array hoge2 = JacksonSerializer.deserialize(json, Array.class);
String json2 = JacksonSerializer.serializeAsString(hoge2);
System.out.println("desirialize success ?");
System.out.println(json.equals(json2));
// 余計な値が入っていても無視してJSONを変換できるか確認
System.out.println("\nmapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\nが有効に効いているか確認する為、適当な値をJSONに挿入");
StringBuilder sb = new StringBuilder(json2);
sb.insert(1, "\"unknwon\":\"fugafuga\",");
String customJson = sb.toString();
System.out.println(customJson);
Array hoge3 = JacksonSerializer.deserialize(customJson, Array.class);
String json3 = JacksonSerializer.serializeAsString(hoge3);
System.out.println("desirialize success ?");
System.out.println(json.equals(json3));
}
public abstract class SuperClass {
public int id;
@JsonIgnore
private String address = "address";
public String getPublic() {
return "public";
}
}
public class SubClass extends SuperClass {
public String name;
}
public class DerivedClass extends SuperClass {
public int number;
}
public class Array extends SuperClass {
public List<SuperClass> list = new ArrayList<SuperClass>();
}
実行結果
{
"id": 0,
"address": "address",
"list": [
"java.util.ArrayList",
[
{
"classType": "SubClass",
"id": 1234,
"address": "address",
"name": "aiueo"
},
{
"classType": "DerivedClass",
"id": 5678,
"address": "address",
"number": 1
}
]
]
}
desirialize success ?
true
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
が有効に効いているか確認する為、適当な値をJSONに挿入
{
"unknwon": "fugafuga",
"id": 0,
"address": "address",
"list": [
"java.util.ArrayList",
[
{
"classType": "SubClass",
"id": 1234,
"address": "address",
"name": "aiueo"
},
{
"classType": "DerivedClass",
"id": 5678,
"address": "address",
"number": 1
}
]
]
}
desirialize success ?
true
制限
一応、ここに書いたテストは通りますが、既に分かっている制限として、
「循環参照」をしているようなオブジェクトがあると失敗します。
2017/02/20 追記
JsonIgnoreAnnotationIgnoreInterceptor
の実装を以下の様に変更する事で、循環参照も問題なくできました。
(ついでにオブジェクトの @JsonTypeInfo を無視する設定も追加 )
rivate static class JsonIgnoreAnnotationIgnoreInterceptor
extends JacksonAnnotationIntrospector {
/** */
private static final long serialVersionUID = 1L;
/**
* フィールドは @JsonIgnore が無い事、メソッドは @JsonIgnore がある事にする
**/
@Override
public boolean _isIgnorable(Annotated a) {
return a instanceof AnnotatedMethod;
}
/**
* メソッド時は @JsonProperty や @JsonView などがあっても null を返す
*/
@Override
public PropertyName findNameForSerialization(Annotated a) {
return a instanceof AnnotatedMethod ?
null : super.findNameForSerialization(a);
}
/**
* メソッド時は @JsonProperty や @JsonView などがあっても null を返す
*/
@Override
public PropertyName findNameForDeserialization(Annotated a) {
return a instanceof AnnotatedMethod ?
null : super.findNameForDeserialization(a);
}
/**
* @JsonTypeInfo を無視
**/
@Override
protected TypeResolverBuilder<?> _findTypeResolver(MapperConfig<?> config,
Annotated ann, JavaType baseType) {
TypeResolverBuilder<?> builder = super._findTypeResolver(config, ann, baseType);
if (builder == null) {
return null;
}
builder.init(JsonTypeInfo.Id.CLASS, null);
builder.inclusion(JsonTypeInfo.As.PROPERTY);
builder.typeProperty("classType");
return builder;
}
/**
* 循環参照対策として、オブジェクトのIDを本文に埋め込み
**/
@Override
public ObjectIdInfo findObjectIdInfo(final Annotated ann) {
return new ObjectIdInfo(
PropertyName.construct("@id", null),
null,
ObjectIdGenerators.IntSequenceGenerator.class,
null);
}
}
2017/03/17 追記
最初にフィールドのみを対象にするように設定しましたが、@JsonProperty などが getter/setter に付いている場合、この設定が無視されて getter/setter を使ってしまう事がわかりました。
// Fieldのみ対象に設定
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
こちらに対しては、次のように @JsonIgnoreがフィールドには無い、メソッドにはあるという形で書き換えてしまう事で対処できます。
@Override
public boolean _isIgnorable(Annotated a) {
return a instanceof AnnotatedMethod;
}
~~それ以外は割と上手くいっている気がしますが、~~何の保証も無いので使う場合は自己責任でお願いします。