2017-08-15 77 views
6

Biorąc pod uwagę następujące POJOs ..Jak serializować zawartość listy do płaskiego obiektu JSON z Jacksonem?

public class City { 

    private String title; 
    private List<Person> people; 
} 

...

public class Person { 

    private String name; 
    private int age; 
} 

Chciałbym niech Jackson instancje serializacji z klas następującemu przykład JSON:

{ 
    "title" : "New York", 
    "personName_1" : "Jane Doe", 
    "personAge_1" : 42, 
    "personName_2" : "John Doe", 
    "personAge_2" : 23 
} 

Format JSON jest zdefiniowany przez zewnętrzny interfejs API, którego nie można zmienić.

I już, że mogę opisywać pole listy z niestandardowych serializatora takich jak:

@JsonSerialize(using = PeopleSerializer.class) 
private List<Person> people; 

... i tutaj jest podstawowym realizacja Próbowałem:

public class PeopleSerializer extends JsonSerializer<List<Person>> { 

    private static final int START_INDEX = 1; 

    @Override 
    public void serialize(List<Person> people, 
          JsonGenerator generator, 
          SerializerProvider provider) throws IOException { 
     for (int i = 0; i < people.size(); ++i) { 
      Person person = people.get(i); 
      int index = i + START_INDEX; 
      serialize(person, index, generator); 
     } 
    } 

    private void serialize(Person person, int index, JsonGenerator generator) throws IOException { 
     generator.writeStringField(getIndexedFieldName("personName", index), 
            person.getName()); 
     generator.writeNumberField(getIndexedFieldName("personAge", index), 
            person.getAge()); 
    } 

    private String getIndexedFieldName(String fieldName, int index) { 
     return fieldName + "_" + index; 
    } 

} 

to jednak nie powiodło się z:

JsonGenerationException: Can not write a field name, expecting a value 

Sprawdziłem również przy użyciu interfejsu Jacksona Converter, ale t nie nadaje się do rozpakowania zagnieżdżonych obiektów list.

Jestem też świadomy @JsonUnwrapped ale nie jest przeznaczony do stosowania z list.

Notki

Podobne posty (deserializacji)

pokrewne biblioteka

+0

Chyba trzeba napisać 'JsonSerializer' dla klasy' City', to dlatego, że serializer dla 'City' pisze nazwa pola 'ludzie' następnie oczekuje, że twój niestandardowy' JsonSerializer' zapisze wartość. –

+0

@ug_ Próbowałem tego i opisałem klasę 'City' za pomocą' @JsonSerialize (using = CitySerializer.class). W ramach serializatora niestandardowego dokonałem tylko serializacji pola 'ludzie', podobnie jak w powyższym' PeopleSerializer'. Serializacja kończy się niepowodzeniem z tym samym komunikatem o błędzie. – JJD

+0

@JJD - W przypadku braku szansy istnieje prostsze podejście, jaki jest główny problem, który próbujesz rozwiązać? Nie mogę sobie wyobrazić przypadku, w którym docelowa struktura JSON ułatwia analizowanie ... – AjahnCharles

Odpowiedz

1

Można użyć BeanSerializerModifier bezpośrednio modyfikować jak nazwa właściwości i wartości są zapisywane. Dzięki temu możesz wykryć, czy niestandardowa adnotacja jest obecna, w tym przypadku utworzyłem jeden o nazwie @FlattenCollection. Gdy adnotacja jest obecna, tablica lub kolekcja nie została zapisana przy użyciu normalnej metody, ale zamiast tego została zapisana przez niestandardowy program piszący (FlattenCollectionPropertyWriter).

Ta adnotacja prawdopodobnie zostanie przerwana na tablicach 2d lub innych skrajnych przypadkach, których nie testowałem, ale prawdopodobnie mógłbyś dla nich napisać bez większych problemów, przynajmniej rzucić sensowny błąd.

Oto pełny działający kod. Ważniejsze punkty są

  • FlattenCollectionSerializerModifier.changeProperties
  • FlattenCollectionPropertyWriter.serializeAsField
  • Para TODO i umieścić tam dla ciebie.

wyjściowa:

{ 
    "titleCity" : "New York", 
    "personName_1" : "Foo", 
    "personAge_1" : 123, 
    "personName_2" : "Baz", 
    "personAge_2" : 22 
} 

Kod:

import com.fasterxml.jackson.core.JsonGenerator; 
import com.fasterxml.jackson.databind.*; 
import com.fasterxml.jackson.databind.ser.*; 
import com.fasterxml.jackson.databind.util.NameTransformer; 

import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 
import java.util.*; 

public class SO45698499 { 


    public static void main(String [] args) throws Exception { 
     ObjectWriter writer = createMapper().writerWithDefaultPrettyPrinter(); 
     String val = writer.writeValueAsString(new City("New York", 
       Arrays.asList(new Person("Foo", 123), new Person("Baz", 22)))); 

     System.out.println(val); 
    } 


    /** 
    * Constructs our mapper with the serializer modifier in mind 
    * @return 
    */ 
    public static ObjectMapper createMapper() { 
     FlattenCollectionSerializerModifier modifier = new FlattenCollectionSerializerModifier(); 
     SerializerFactory sf = BeanSerializerFactory.instance.withSerializerModifier(modifier); 
     ObjectMapper mapper = new ObjectMapper(); 
     mapper.setSerializerFactory(sf); 

     return mapper; 
    } 

    @Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) 
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface FlattenCollection { 
    } 

    /** 
    * Looks for the FlattenCollection annotation and modifies the bean writer 
    */ 
    public static class FlattenCollectionSerializerModifier extends BeanSerializerModifier { 

     @Override 
     public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) { 
      for (int i = 0; i < beanProperties.size(); i++) { 
       BeanPropertyWriter writer = beanProperties.get(i); 
       FlattenCollection annotation = writer.getAnnotation(FlattenCollection.class); 
       if (annotation != null) { 
        beanProperties.set(i, new FlattenCollectionPropertyWriter(writer)); 
       } 
      } 
      return beanProperties; 
     } 
    } 

    /** 
    * Instead of writing a collection as an array, flatten the objects down into values. 
    */ 
    public static class FlattenCollectionPropertyWriter extends BeanPropertyWriter { 
     private final BeanPropertyWriter writer; 

     public FlattenCollectionPropertyWriter(BeanPropertyWriter writer) { 
      super(writer); 
      this.writer = writer; 
     } 

     @Override 
     public void serializeAsField(Object bean, 
            JsonGenerator gen, 
            SerializerProvider prov) throws Exception { 
      Object arrayValue = writer.get(bean); 

      // lets try and look for array and collection values 
      final Iterator iterator; 
      if(arrayValue != null && arrayValue.getClass().isArray()) { 
       // deal with array value 
       iterator = Arrays.stream((Object[])arrayValue).iterator(); 
      } else if(arrayValue != null && Collection.class.isAssignableFrom(arrayValue.getClass())) { 
       iterator = ((Collection)arrayValue).iterator(); 
      } else { 
       iterator = null; 
      } 

      if(iterator == null) { 
       // TODO: write null? skip? dunno, you gonna figure this one out 
      } else { 
       int index=0; 
       while(iterator.hasNext()) { 
        index++; 
        Object value = iterator.next(); 
        if(value == null) { 
         // TODO: skip null values and still increment or maybe dont increment? You decide 
        } else { 
         // TODO: OP - update your prefix/suffix here, its kinda weird way of making a prefix 
         final String prefix = value.getClass().getSimpleName().toLowerCase(); 
         final String suffix = "_"+index; 
         prov.findValueSerializer(value.getClass()) 
           .unwrappingSerializer(new FlattenNameTransformer(prefix, suffix)) 
           .serialize(value, gen, prov); 
        } 
       } 
      } 
     } 
    } 

    public static class FlattenNameTransformer extends NameTransformer { 

     private final String prefix; 
     private final String suffix; 

     public FlattenNameTransformer(String prefix, String suffix) { 
      this.prefix = prefix; 
      this.suffix = suffix; 
     } 

     @Override 
     public String transform(String name) { 
      // captial case the first letter, to prepend the suffix 
      String transformedName = Character.toUpperCase(name.charAt(0)) + name.substring(1); 
      return prefix + transformedName + suffix; 
     } 
     @Override 
     public String reverse(String transformed) { 
      if (transformed.startsWith(prefix)) { 
       String str = transformed.substring(prefix.length()); 
       if (str.endsWith(suffix)) { 
        return str.substring(0, str.length() - suffix.length()); 
       } 
      } 
      return null; 
     } 
     @Override 
     public String toString() { return "[FlattenNameTransformer('"+prefix+"','"+suffix+"')]"; } 
    } 


    /*=============================== 
    * POJOS 
    ===============================*/ 
    public static class Person { 
     private String name; 
     private int age; 

     public Person(String name, int age) { 
      this.name = name; 
      this.age = age; 
     } 

     public String getName() { 
      return name; 
     } 

     public void setName(String name) { 
      this.name = name; 
     } 

     public int getAge() { 
      return age; 
     } 

     public void setAge(int age) { 
      this.age = age; 
     } 
    } 

    public static class City { 
     private String titleCity; 
     private List<Person> people; 

     public City(String title, List<Person> people) { 
      this.titleCity = title; 
      this.people = people; 
     } 

     public String getTitleCity() { 
      return titleCity; 
     } 

     public void setTitleCity(String titleCity) { 
      this.titleCity = titleCity; 
     } 

     @FlattenCollection 
     public List<Person> getPeople() { 
      return people; 
     } 

     public void setPeople(List<Person> people) { 
      this.people = people; 
     } 
    } 
} 
+0

WOW! Imponujące! Skąd dowiedziałeś się o tej opcji, aby dostosować serializację? - Z powodzeniem zintegrowałem klasy w zakresie instancji 'FlattenCollectionPropertyWriter', jednak nazwa' serializeAsField() 'jest ** nie **. – JJD

+0

@JJD Zapomniałem, gdzie znalazłem pierwotnie, odświeżyłem pamięć z dużą ilością wyszukiwania java doc w moim IDE.Jeśli 'serializeAsField()' nie jest wywoływane, możesz sprawdzić inne metody 'seralizeAs ***' w 'BeanPropertyWriter'. Upewnij się także, że tworzony jest 'FlattenCollectionPropertyWriter'. Te metody byłyby wywoływane w konkretnych scenariuszach opisywanych przez ich javadoc, jednak mam kłopot z wyobrażeniem sobie, dlaczego mieliby być wezwani do tej własności, ale byłoby to dobre miejsce na przełamanie. –

+0

W jakiś sposób metoda jest dziś wywoływana. Naprawdę nie mogę powiedzieć, co poszło nie tak wczoraj. Może było po prostu za późno. - Dziękuję za Twoją odpowiedź. Świetna pomoc! – JJD

1

podstawie this link Podejrzewam adnotacji pola poziomie tylko delegaci Zapis wartości nie całe właściwości.

A (raczej kludgey) Rozwiązaniem może być posiadanie własnego serializatora dla całej klasy Miasto:

@JsonSerialize(using = CitySerializer.class) 
public class City { 
    private String title; 
    @JsonIgnore 
    private List<Person> people; 
} 

...a następnie

public class CitySerializer extends JsonSerializer<City> { 

    private static final int START_INDEX = 1; 

    @Override 
    public void serialize(City city, 
          JsonGenerator generator, 
          SerializerProvider provider) throws IOException { 
     generator.writeStartObject(); 

     // Write all properties (except ignored) 
     JavaType javaType = provider.constructType(City.class); 
     BeanDescription beanDesc = provider.getConfig().introspect(javaType); 
     JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(provider, 
       javaType, 
       beanDesc); 
     serializer.unwrappingSerializer(null).serialize(value, jgen, provider);` 

     // Custom serialization of people 
     List<Person> people = city.getPeople(); 
     for (int i = 0; i < people.size(); ++i) { 
      Person person = people.get(i); 
      int index = i + START_INDEX; 
      serialize(person, index, generator); 
     } 

     generator.writeEndObject(); 
    } 

    private void serialize(Person person, int index, JsonGenerator generator) throws IOException { 
     generator.writeStringField(getIndexedFieldName("personName", index), 
            person.getName()); 
     generator.writeNumberField(getIndexedFieldName("personAge", index), 
            person.getAge()); 
    } 

    private String getIndexedFieldName(String fieldName, int index) { 
     return fieldName + "_" + index; 
    } 

} 
+0

Dziękuję, to wygląda obiecująco. Jedną dużą wadą jest jednak to, że nie mogę ** po prostu ** zdefiniować specjalnej obsługi dla pola 'people'. Muszę też ręcznie wpisać ** wszystkie inne ** pola (jest ich więcej w rzeczywistości) klasy "City". Ilekroć zmieni się klasa 'City', będę musiał również zaktualizować serializer. To sprawia, że ​​trudno ją utrzymać. Omówiono także [tutaj] (https://stackoverflow.com/questions/14714328/jackson-how-to-add-custom-property-to-the-json- bez-modyfikując- -pojo). – JJD

+0

Byłoby wspaniale, gdybym mógł ** przechwycić ** serializację 'City' i ręcznie napisać niestandardową reprezentację JSON pola' people'. Można to również nazwać * serializatorem delegacji * dla określonego pola. Może, po prostu szukam według złych warunków. – JJD

+0

@JJD - Całkowicie się z tobą zgadzam. Uderzyłem w to, ale nie mogę teraz łatwo przetestować. – AjahnCharles