LoginSignup
0
0

More than 1 year has passed since last update.

【Jackson】共通のunboxするシリアライズ処理を実装する

Posted at

TL;DR

  • 当該の抽象型向けのStdSerializerを定義し、適切に登録することで実現できる
  • 中身の型ごとのシリアライズは、SerializerProvider::findValueSerializerで取得したシリアライザーで行える

やること

以下のような、値をラップする抽象型が有るとします。
用途としては、DDDでいう値オブジェクトを定義する時、共通の振る舞いを持たせるために基底となる型を作る場合を想定しています。

package com.wrongwrong.values;

public abstract class AbstractValue<T extends Comparable<T>> {
    public abstract T getValue();
}

このクラスは、以下のように継承されて使われます。

intでの継承例
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アノテーションが設定されている場合

今回のサンプルで取り上げているAbstractValueComparableなら何でも設定可能なため、これらのロジックを考慮する必要が有ります。
これを行っているのが以下の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辺りで設定することができます。

動かしてみる

最後に、実際に動かした様子を紹介します。
型ごとに適切なシリアライズが行われていることが分かります。

シリアライズ対象のフィールドとなるクラス

中身はint
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; }
}
中身はString
public class Bar extends AbstractValue<String> {
    private final String value;

    public Bar(String value) { this.value = value; }

    @Override
    public String getValue() { return value; }
}
中身はObject
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())
        );
    }
}

  1. AbstractValueに表示用ロジックが入ってしまうのは責務が漏れていると言えるため。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0