Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

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

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

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

テストコード

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away