web-dev-qa-db-ja.com

ジャクソンによる多態型の逆シリアル化

私がそのようなクラス構造を持っている場合:

public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}

別の@JsonTypeInfoを逆シリアル化する別の方法はありますか?私の親クラスでこの注釈を使用します:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "objectType")

APIのクライアントに"objectType": "SubClassA"を含めてParentサブクラスを逆シリアル化するように強制する必要はありません。

@JsonTypeInfoを使用する代わりに、Jacksonはサブクラスに注釈を付け、一意のプロパティを介して他のサブクラスと区別する方法を提供しますか?上記の私の例では、これは「JSONオブジェクトに"stringA": ...がある場合はSubClassAとしてデシリアライズし、"stringB": ...がある場合はSubClassBとしてデシリアライズする」のようになります。 。

25
Sam Berry

これは@JsonTypeInfo@JsonSubTypesを使用する必要があるように感じますが、ドキュメントを選択しましたが、提供できるプロパティのどれも、あなたが説明しているものとまったく一致していないようです。

@JsonSubTypes 'の "name"プロパティと "value"プロパティを非標準的な方法で使用するカスタムデシリアライザを作成して、目的を達成できます。デシリアライザと@JsonSubTypesは基本クラスで提供され、デシリアライザは「名前」の値を使用してプロパティの存在を確認し、存在する場合は、JSONを「値」で提供されるクラスにデシリアライズします。 "プロパティ。クラスは次のようになります。

@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
        @Type(name = "stringA", value = SubClassA.class),
        @Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
    private Long id;
    ...
}

public class SubClassA extends Parent {
    private String stringA;
    private Integer intA;
    ...
}

public class SubClassB extends Parent {
    private String stringB;
    private Integer intB;
    ...
}
21
Erik Gillespie

ここに私が思いついた解決策があり、それはエリック・ガレスピーのものを少し拡張します。それはあなたが望んだことを正確に行い、私にとってはうまくいきました。

Jackson 2.9の使用

@JsonDeserialize(using = CustomDeserializer.class)
public abstract class BaseClass {

    private String commonProp;
}

// Important to override the base class' usage of CustomDeserializer which produces an infinite loop
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassA extends BaseClass {

    private String classAProp;
}

@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassB extends BaseClass {

    private String classBProp;
}

public class CustomDeserializer extends StdDeserializer<BaseClass> {

    protected CustomDeserializer() {
        super(BaseClass.class);
    }

    @Override
    public BaseClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        TreeNode node = p.readValueAsTree();

        // Select the concrete class based on the existence of a property
        if (node.get("classAProp") != null) {
            return p.getCodec().treeToValue(node, ClassA.class);
        }
        return p.getCodec().treeToValue(node, ClassB.class);
    }
}

// Example usage
String json = ...
ObjectMapper mapper = ...
BaseClass instance = mapper.readValue(json, BaseClass.class);

手の込んだものにしたい場合は、CustomDeserializerを展開して、存在する場合に特定のクラスにマップするプロパティ名をマップするMap<String, Class<?>>を含めることができます。このようなアプローチは、この 記事 に示されています。

ところで、これを要求するgithubの問題があります: https://github.com/FasterXML/jackson-databind/issues/1627

21
bernie

いいえ。そのような機能は要求されています。「型推論」または「暗黙の型」と呼ばれる可能性がありますが、これがどのように機能するかについて、実用的な一般的な提案はまだ出されていません。特定のケースに対する特定のソリューションをサポートする方法を考えるのは簡単ですが、一般的なソリューションを理解することはより困難です。

8
StaxMan

他の人が指摘したように、 どのように機能するかについてコンセンサスがないため、実装されていません

クラスFoo、Bar、およびそれらの親FooBarソリューションがある場合、次のようなJSONがあるとかなり明白になります。

{
  "foo":<value>
}

または

{
  "bar":<value>
}

しかし、あなたが得たときに何が起こるかについての一般的な答えはありません

{
  "foo":<value>,
  "bar":<value>
}

一見すると、最後の例は400 Bad Requestの明らかなケースのように見えますが、実際には多くの異なるアプローチがあります。

  1. 400 Bad Requestとして処理する
  2. タイプ/フィールドによる優先順位(たとえば、フィールドエラーが存在する場合、他のフィールドfooよりも優先順位が高くなります)
  3. より複雑な2。

ほとんどの場合に機能し、既存のJacksonインフラストラクチャをできるだけ活用しようとする現在のソリューションは次のとおりです(階層ごとに1つのデシリアライザーのみが必要です)。

public class PresentPropertyPolymorphicDeserializer<T> extends StdDeserializer<T> {

    private final Map<String, Class<?>> propertyNameToType;

    public PresentPropertyPolymorphicDeserializer(Class<T> vc) {
        super(vc);
        this.propertyNameToType = Arrays.stream(vc.getAnnotation(JsonSubTypes.class).value())
                                        .collect(Collectors.toMap(Type::name, Type::value,
                                                                  (a, b) -> a, LinkedHashMap::new)); // LinkedHashMap to support precedence case by definition order
    }

    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
        ObjectNode object = objectMapper.readTree(p);
        for (String propertyName : propertyNameToType.keySet()) {
            if (object.has(propertyName)) {
                return deserialize(objectMapper, propertyName, object);
            }
        }

        throw new IllegalArgumentException("could not infer to which class to deserialize " + object);
    }

    @SuppressWarnings("unchecked")
    private T deserialize(ObjectMapper objectMapper,
                          String propertyName,
                          ObjectNode object) throws IOException {
        return (T) objectMapper.treeToValue(object, propertyNameToType.get(propertyName));
    }
}

使用例:

@JsonSubTypes({
        @JsonSubTypes.Type(value = Foo.class, name = "foo"),
        @JsonSubTypes.Type(value = Bar.class, name = "bar"),
})
interface FooBar {
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Foo implements FooBar {
    private final String foo;
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Bar implements FooBar {
    private final String bar;
}

ジャクソン構成

SimpleModule module = new SimpleModule();
module.addDeserializer(FooBar.class, new PresentPropertyPolymorphicDeserializer<>(FooBar.class));
objectMapper.registerModule(module);

または、Spring Bootを使用している場合:

@JsonComponent
public class FooBarDeserializer extends PresentPropertyPolymorphicDeserializer<FooBar> {

    public FooBarDeserializer() {
        super(FooBar.class);
    }
}

テスト:

    @Test
    void shouldDeserializeFoo() throws IOException {
        // given
        var json = "{\"foo\":\"foo\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    }

    @Test
    void shouldDeserializeBar() throws IOException {
        // given
        var json = "{\"bar\":\"bar\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Bar("bar"));

    }

    @Test
    void shouldDeserializeUsingAnnotationDefinitionPrecedenceOrder() throws IOException {
        // given
        var json = "{\"bar\":\"\", \"foo\": \"foo\"}";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    }
6
lpandzic

私のアプリでは古い構造を保持する必要があるため、データを変更せずに多態性をサポートする方法を見つけました。これが私がすることです:

  1. JsonDeserializerを拡張する
  2. ツリーに変換してフィールドを読み取り、サブクラスオブジェクトを返す

    @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonNode jsonNode = p.readValueAsTree(); 
        Iterator<Map.Entry<String, JsonNode>> ite = jsonNode.fields();
        boolean isSubclass = false;
        while (ite.hasNext()) {
            Map.Entry<String, JsonNode> entry = ite.next();
            // **Check if it contains field name unique to subclass**
            if (entry.getKey().equalsIgnoreCase("Field-Unique-to-Subclass")) {
                isSubclass = true;
                break;
            }
        }
        if (isSubclass) {
            return mapper.treeToValue(jsonNode, SubClass.class);
        } else {
            // process other classes
        }
    }
    
3
Nicholas Ng