Jackson でオブジェクトのシリアライザ/デシリアライザを作ってみる

# Jackson でオブジェクトのシリアライザ/デシリアライザを作ってみる

Jacksonでオブジェクトのシリアライザを作成してみます。
通常の ObjectMapper の設定のままだと getter に対する setter が必要だったり、ジェネリクスが扱えなかったりと色々条件があるので、ObjectMapper に次の設定を施して実施します。

> * 全フィールド値のみ対象とする（getter/setter は対象としない）
* ジェネリクスに対応する為、自身の型を埋め込む
* @JsonIgnoreが付いていてもシリアライズ対象にする
* null は出力しない（データ量削減）

ObjectMapper 側で色々設定する事で、オブジェクト側は特に実装の変更を必要としない点がミソです。

### シリアライザ/デシリアライザソース

```java:JacksonSerializer.java
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);
    }
}

```

### テストコード

```java
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>();
}
    
```

### 実行結果

```text:実行結果

{
    "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

```

    
## 制限

~~一応、ここに書いたテストは通りますが、既に分かっている制限として、~~
~~「<b>循環参照</b>」をしているようなオブジェクトがあると失敗します。~~

---
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;
        }
```
---

~~それ以外は割と上手くいっている気がしますが、~~何の保証も無いので使う場合は自己責任でお願いします。