web-dev-qa-db-fra.com

Les @JsonSubTypes de Jackson sont-ils encore nécessaires pour la désérialisation polymorphe?

Je suis capable de sérialiser et de désérialiser une hiérarchie de classes où la classe de base abstraite est annotée avec

@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")

mais aucun @JsonSubTypes ne répertorie les sous-classes, et les sous-classes elles-mêmes sont relativement non annotées, n'ayant qu'un @JsonCreator sur le constructeur. ObjectMapper est Vanilla, et je n'utilise pas de mixin.

La documentation de Jackson sur PolymorphicDeserialization et "type ids" suggère (fortement) que j'ai besoin de l'annotation @JsonSubTypes Sur la classe de base abstraite, ou que je l'utilise sur un mixin, ou que j'ai besoin de - enregistre les sous-types avec ObjectMapper . Et il y a beaucoup de SO questions et/ou billets de blogs qui sont d’accord. Pourtant, cela fonctionne. ( Ceci est Jackson 2.6.0. )

Alors ... suis-je le bénéficiaire d'une fonctionnalité non encore documentée ou est-ce que je m'appuie sur un comportement non documenté (qui peut changer) ou est-ce qu'il se passe autre chose? (Je demande parce que je ne veux vraiment pas que ce soit l'un des deux derniers. Mais je dois savoir .)

EDIT: Ajout de code - et un commentaire. Le commentaire est le suivant: j'aurais dû mentionner que toutes les sous-classes que je désérialise sont dans le même package et le même fichier jar que la classe abstraite de base.

Classe de base abstraite:

package so;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")
public abstract class PolyBase
{
    public PolyBase() { }

    @Override
    public abstract boolean equals(Object obj);
}

Une sous-classe de celui-ci:

package so;
import org.Apache.commons.lang3.builder.EqualsBuilder;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public final class SubA extends PolyBase
{
    private final int a;

    @JsonCreator
    public SubA(@JsonProperty("a") int a) { this.a = a; }

    public int getA() { return a; }

    @Override
    public boolean equals(Object obj) {
        if (null == obj) return false;
        if (this == obj) return true;
        if (this.getClass() != obj.getClass()) return false;

        SubA rhs = (SubA) obj;
        return new EqualsBuilder().append(this.a, rhs.a).isEquals();
    }
}

Les sous-classes SubB et SubC sont identiques, sauf que le champ a est déclaré String (et non int) dans SubB. et boolean (pas int) dans SubC (et la méthode getA est modifiée en conséquence).

Classe de test:

package so;    
import Java.io.IOException;
import org.Apache.commons.lang3.builder.EqualsBuilder;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.*;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestPoly
{
    public static class TestClass
    {
        public PolyBase pb1, pb2, pb3;

        @JsonCreator
        public TestClass(@JsonProperty("pb1") PolyBase pb1,
                         @JsonProperty("pb2") PolyBase pb2,
                         @JsonProperty("pb3") PolyBase pb3)
        {
            this.pb1 = pb1;
            this.pb2 = pb2;
            this.pb3 = pb3;
        }

        @Override
        public boolean equals(Object obj) {
            if (null == obj) return false;
            if (this == obj) return true;
            if (this.getClass() != obj.getClass()) return false;

            TestClass rhs = (TestClass) obj;
            return new EqualsBuilder().append(pb1, rhs.pb1)
                                      .append(pb2, rhs.pb2)
                                      .append(pb3, rhs.pb3)
                                      .isEquals();
        }
    }

    @Test
    public void jackson_should_or_should_not_deserialize_without_JsonSubTypes() {

        // Arrange
        PolyBase pb1 = new SubA(5), pb2 = new SubB("foobar"), pb3 = new SubC(true);
        TestClass sut = new TestClass(pb1, pb2, pb3);

        ObjectMapper mapper = new ObjectMapper();

        // Act
        String actual1 = null;
        TestClass actual2 = null;

        try {
            actual1 = mapper.writeValueAsString(sut);
        } catch (IOException e) {
            fail("didn't serialize", e);
        }

        try {
            actual2 = mapper.readValue(actual1, TestClass.class);
        } catch (IOException e) {
            fail("didn't deserialize", e);
        }

        // Assert
        assertThat(actual2).isEqualTo(sut);
    }
}

Ce test réussit et si vous cassez à la deuxième ligne try {, Vous pouvez inspecter actual1 Et voir:

{"pb1":{"@class":".SubA","a":5},
 "pb2":{"@class":".SubB","a":"foobar"},
 "pb3":{"@class":".SubC","a":true}}

Les trois sous-classes ont donc été correctement sérialisées (chacune avec leur nom de classe comme id), puis désérialisées, et le résultat comparé est égal (chaque sous-classe a un "type valeur" equals()).

43
davidbak

Il existe deux manières de réaliser un polymorphisme en sérialisation et en désérialisation avec Jackson. Ils sont définis dans la section 1. Utilisation dans le lien que vous avez publié.

Votre code

@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")

est un exemple de la deuxième approche. La première chose à noter est que

Toutes les instances de type annoté et ses sous-types utilisent ces paramètres (sauf substitution par une autre annotation)

Donc, cette valeur de configuration se propage à tous les sous-types. Ensuite, nous avons besoin d’un identifiant de type qui mappera un type Java) à une valeur de texte dans la chaîne JSON et inversement. Dans votre exemple, il est donné par JsonTypeInfo.Id#MINIMAL_CLASS

Signifie que Java nom de classe avec chemin minimal est utilisé comme identificateur de type.

Ainsi, un nom de classe minimal est généré à partir de l'instance cible et écrit dans le contenu JSON lors de la sérialisation. Ou un nom de classe minimal est utilisé pour déterminer le type de cible pour la désérialisation.

Vous auriez aussi pu utiliser JsonTypeInfo.Id#NAME lequel

Signifie que nom de type logique est utilisé comme information de type; name devra alors être résolu séparément en un type concret réel (Class).

Pour fournir un tel nom de type logique, vous utilisez @JsonSubTypes

Annotation utilisée avec JsonTypeInfo pour indiquer les sous-types de types polymorphes sérialisables et associer les noms logiques utilisés dans le contenu JSON (qui est plus portable) que d’utiliser des physiques Java).

Ceci est juste un autre moyen d'atteindre le même résultat. La documentation que vous demandez sur les états

Les identifiants de type basés sur Java sont assez simples: il s'agit simplement du nom de la classe, éventuellement d'une simple suppression du préfixe (pour la variante "minimale"). Mais le nom du type est différent: on a avoir une correspondance entre le nom logique et la classe réelle.

Donc, les divers JsonTypeInfo.Id Les valeurs qui traitent des noms de classe sont simples car elles peuvent être générées automatiquement. Pour les noms de type, toutefois, vous devez indiquer explicitement la valeur de mappage.

47