2015-07-28 16 views
26

Sono in grado di serializzare e deserializzare una gerarchia di classi in cui la classe astratta di base è annotata conJackson's @JsonSubTypes è ancora necessario per la deserializzazione polimorfica?

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

ma non @JsonSubTypes elencando le sottoclassi, e le sottoclassi stessi sono relativamente non annotate, avendo solo una @JsonCreator sul costruttore. ObjectMapper è vanilla e non sto usando un mixin.

Jackson documentazione su PolymorphicDeserialization and "type ids" suggerisce (fortemente) Ho bisogno del @JsonSubTypes annotazioni sulla classe di base astratta, o utilizzarlo su un mixin, o che ho bisogno di register the subtypes with the ObjectMapper. E ci sono un sacco di domande SO e/o post sul blog che sono d'accordo. Eppure funziona. (Questo è Jackson 2.6.0.)

Quindi ... sono io il beneficiario di una caratteristica non ancora privi di documenti o sto contando sul comportamento privi di documenti (che può cambiare) o è qualcos'altro? (Le sto chiedendo perché non voglio che sia tra le ultime due. Ma I gots to know.)

MODIFICA: aggiunta di codice e un commento. Il commento è: avrei dovuto dire che tutte le sottoclassi che sto deserializzando sono nello stesso pacchetto e nello stesso vaso della classe astratta base.

classe astratta di base:

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

Una sottoclasse di esso:

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

sottoclassi SubB e SubC sono gli stessi, tranne quel campo a è (non int) ha dichiarato String in SubB e boolean (non int) in SubC (e il metodo getA è modifi ed di conseguenza).

classe

prova:

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

Questo test viene superato e se si rompe la seconda riga try { è possibile esaminare actual1 e vedere:

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

Così i tre sottoclassi ricevuti correttamente serializzati (ognuno con il loro nome di classe come id) e quindi deserializzato e il risultato è paragonato (ogni sottoclasse ha un "tipo di valore" equals()).

+5

Buona domanda, buon esempio. Mostra una buona ricerca. Ne hai bisogno di più. –

risposta

26

Esistono due modi per ottenere il polimorfismo in serializzazione e deserializzazione con Jackson. Sono definiti in Sezione 1. Utilizzo nel link pubblicato.

Il codice

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

è un esempio del secondo approccio.La prima cosa da notare è che

Tutte le istanze di tipo annotato e dei suoi sottotipi utilizzare queste impostazioni (a meno che non prevalga un'altra annotazione)

Quindi questo valore di configurazione si propaga a tutti i sottotipi. Quindi, abbiamo bisogno di un identificatore di tipo che mapperà un tipo Java a un valore di testo nella stringa JSON e viceversa. Nel tuo esempio, questo è dato da JsonTypeInfo.Id#MINIMAL_CLASS

Indica che il nome della classe Java con percorso minimo viene utilizzato come identificativo del tipo.

Quindi un nome classe minimo viene generato dall'istanza di destinazione e scritto nel contenuto JSON durante la serializzazione. Oppure viene utilizzato un nome di classe minimo per determinare il tipo di target per deserializzare.

Si potrebbe aver usato anche JsonTypeInfo.Id#NAME che

Significa che logico tipo di nome è utilizzato come le informazioni sul tipo; il nome sarà quindi dovrà essere risolto separatamente sul tipo concreto effettivo (Class).

di fornire tale nome di tipo logico, si utilizza @JsonSubTypes

annotazione usato con JsonTypeInfo per indicare sub tipi di tipi polimorfici serializzabili, e ai nomi logici associare utilizzati all'interno del contenuto JSON (che è più portatile rispetto all'utilizzo di nomi di classi Java fisici).

Questo è solo un altro modo per ottenere lo stesso risultato. La documentazione si sta chiedendo sugli stati

tipo IDS che si basano su Java nome della classe sono abbastanza straight-forward: il suo nome solo di classe, forse qualche semplice rimozione del prefisso (per la variante "minimo"). Ma il nome del tipo è diverso: uno ha per avere un mapping tra il nome logico e la classe reale.

Quindi i vari valori JsonTypeInfo.Id relativi ai nomi di classe sono diretti perché possono essere generati automaticamente. Per i nomi di tipo, tuttavia, è necessario fornire il valore di mappatura in modo esplicito.

+1

Grazie per aver spiegato che @JsonSubTypes è necessario solo se si utilizza il modulo NAME. Tutta la documentazione che ho letto e non l'ho capito ... – davidbak

+6

Il problema è che se si utilizza il modulo Nome (che è ampiamente preferibile), non è possibile aggiungere in modo dinamico classi o aggiungere classi in un secondo momento. – HDave

+3

Non è più logico utilizzare JsonTypeInfo sulle singole classi concrete? Usarlo sull'interfaccia sembra violare il principio di "open-close", suppongo (come in abbiamo bisogno di modificare la nostra interfaccia su ogni nuova aggiunta di un'implementazione concreta). Per favore correggimi se mi manca qualcosa qui – Harshit