2014-07-22 12 views
10

Ho il seguente JSON:deserializzazione non riesce per una classe che implementa Collection con Jackson

{ 
    "item": [ 
    { "foo": 1 }, 
    { "foo": 2 } 
    ] 
} 

Questo è fondamentalmente un oggetto che contiene un insieme di elementi.

così ho fatto una classe di deserializzare che:

public class ItemList { 
    @JsonProperty("item") 
    List<Item> items; 

    // Getters, setters & co. 
    // ... 
} 

Tutto funziona bene fino a questo punto.

Ora, per semplificare la vita da qualche altra parte, ho deciso che sarebbe stato bello poter iterare sull'oggetto ItemList e lasciare che implementasse l'interfaccia Collection.

Quindi, in pratica la mia classe è diventato:

public class ItemList implements Collection<Item>, Iterable<Item> { 
    @JsonProperty("item") 
    List<Item> items; 

    // Getters, setters & co. 

    // Generated all method delegates to items. For instance: 
    public Item get(int position) { 
    return items.get(position); 
    } 
} 

L'applicazione funziona correttamente e bene. Tuttavia, la deserializzazione ora fallisce.

Sembra che Jackson si confondersi:

com.fasterxml.jackson.databind.JsonMappingException: Non può deserializzare istanza di com.example.ItemList fuori START_OBJECT gettone

ho ho provato ad aggiungere @JsonDeserialize(as=ItemList.class) ma non ha funzionato.

Qual è la strada da percorrere?

+0

Sono abbastanza sicuro che http://stackoverflow.com/users/22656/jon-skeet sarà in grado di rispondere a questa. Inoltre, potresti essere in grado di trovare alcune informazioni utili in http://stackoverflow.com/a/21279016/1382251. –

+1

Mentre questa è una buona domanda così com'è, se tutto ciò che vuoi è iterare, perché rendere la tua classe una raccolta invece che semplicemente iterabile? – chrylis

+0

@chrylis: così posso "(Articolo t: itemList)" –

risposta

4

Ovviamente non funziona perché Jackson utilizza il deserializzatore di raccolta standard per i tipi di raccolta Java che non conosce nulla sulle proprietà ItemList.

È possibile farlo funzionare ma non in modo molto elegante. È necessario configurare ObjectMapper per sostituire il deserializzatore di raccolta predefinito su un deserializzatore del bean creato manualmente per il tipo corrispondente. Ho scritto un esempio che fa questo in BeanDeserializerModifier per tutte le classi annotate con un'annotazione personalizzata.

Nota che devo ignorare ObjectMapper per avere accesso a un metodo protetto createDeserializationContext di ObjectMapper per creare un contesto deserialisation corretta in quanto il modificatore di fagioli non ha accesso ad essa.

Ecco il codice:

public class JacksonCustomList { 
    public static final String JSON = "{\n" + 
      " \"item\": [\n" + 
      " { \"foo\": 1 },\n" + 
      " { \"foo\": 2 }\n" + 
      " ]\n" + 
      "} "; 

    @Retention(RetentionPolicy.RUNTIME) 
    public static @interface PreferBeanDeserializer { 

    } 

    public static class Item { 
     public int foo; 

     @Override 
     public String toString() { 
      return String.valueOf(foo); 
     } 
    } 

    @PreferBeanDeserializer 
    public static class ItemList extends ArrayList<Item> { 
     @JsonProperty("item") 
     public List<Item> items; 

     @Override 
     public String toString() { 
      return items.toString(); 
     } 
    } 

    public static class Modifier extends BeanDeserializerModifier { 
     private final MyObjectMapper mapper; 

     public Modifier(final MyObjectMapper mapper) { 
      this.mapper = mapper; 
     } 

     @Override 
     public JsonDeserializer<?> modifyCollectionDeserializer(
       final DeserializationConfig config, 
       final CollectionType type, 
       final BeanDescription beanDesc, 
       final JsonDeserializer<?> deserializer) { 
      if (type.getRawClass().getAnnotation(PreferBeanDeserializer.class) != null) { 
       DeserializationContext context = mapper.createContext(config); 
       try { 
        return context.getFactory().createBeanDeserializer(context, type, beanDesc); 
       } catch (JsonMappingException e) { 
        throw new IllegalStateException(e); 
       } 

      } 
      return super.modifyCollectionDeserializer(config, type, beanDesc, deserializer); 
     } 
    } 

    public static class MyObjectMapper extends ObjectMapper { 
     public DeserializationContext createContext(final DeserializationConfig cfg) { 
      return super.createDeserializationContext(getDeserializationContext().getParser(), cfg); 
     } 
    } 

    public static void main(String[] args) throws IOException { 
     final MyObjectMapper mapper = new MyObjectMapper(); 
     SimpleModule module = new SimpleModule(); 
     module.setDeserializerModifier(new Modifier(mapper)); 

     mapper.registerModule(module); 
     System.out.println(mapper.readValue(JSON, ItemList.class)); 
    } 

} 
+1

+1 Nizza. Ho impiegato un paio d'ore a cercare di trovare una soluzione simile usando 'BeanDeserializerModifier.modifyCollectionDeserializer', ma non sono riuscito a capire come ottenere un' DeserializationContext'. –

+0

Sembra un modo elegante per gestirlo, ci provo. –

+1

Un suggerimento: invece di sottoclasse di 'ObjectMapper', dovresti essere in grado di combinare' ContextualDeserializer' con deserializer di raccolta personalizzato: questo ti dà accesso a trovare/modificare valore deserializzatore che è necessario, e garantisce anche che sia inizializzato correttamente. Può essere sensato dare un'occhiata ai serializzatori di raccolta predefiniti per gestire alcuni casi speciali (supporto per i tipi polimorfici). – StaxMan

2

Se si considera la proprietà item essere il valore principale, è possibile che cambiare la classe ItemList come segue, utilizzando il @JsonRootNameannotation:

@JsonRootName("item") 
public class ItemList implements Collection<Item>, Iterable<Item> { 
    private List<Item> items = new ArrayList<>(); 

    public Item get(int position) { 
     return items.get(position); 
    } 

    // implemented methods deferring to delegate 
    // ... 
} 

Se quindi si attiva il UNWRAP_ROOT_VALUEdeserialization feature, le cose funzionano come previsto :

String json = "{\"item\": [{\"foo\": 1}, {\"foo\": 2}]}"; 
ObjectMapper mapper = new ObjectMapper(); 
ObjectReader reader = mapper.reader(ItemList.class); 

ItemList itemList = reader 
     .with(DeserializationFeature.UNWRAP_ROOT_VALUE) 
     .readValue(json); 

serializzazione funziona altrettanto bene, con la WRAP_ROOT_VALUEserialization feature abilitato:

ObjectMapper mapper = new ObjectMapper(); 
ObjectWriter writer = mapper.writer(); 

Item item1 = new Item(); 
item1.setFoo(1); 

Item item2 = new Item(); 
item2.setFoo(2); 

ItemList itemList = new ItemList(); 
itemList.add(item1); 
itemList.add(item2); 

String json = writer 
     .with(SerializationFeature.WRAP_ROOT_VALUE) 
     .writeValueAsString(itemList); 

// json contains {"item":[{"foo":1},{"foo":2}]} 

Questa soluzione, ovviamente, non è sufficiente se il vostro ItemList contiene proprietà aggiuntive (diversi ha l'elenco) che sarà anche bisogno di essere serializzato/deserializzato.

+0

Un bel workaround, ma mi piacerebbe mantenere la possibilità di gestire proprietà aggiuntive per il futuro. –

Problemi correlati