web-dev-qa-db-fra.com

Comment sérialiser un contenu de liste en un objet JSON plat avec Jackson?

Compte tenu des POJO suivants.

public class City {

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

...

public class Person {

    private String name;
    private int age;
}

Je voudrais laisser Jackson sérialiser les instances des classes au exemple JSON suivant:

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

Le format JSON est défini par une API externe que je ne peux pas modifier.

J'ai déjà constaté que je pouvais annoter le champ de liste avec un sérialiseur personnalisé tel que:

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

... et voici une implémentation de base que j'ai essayée:

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

}

Cependant, cela échoue avec un:

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

J'ai également envisagé d'utiliser l'interface convertisseur de Jackson, mais cela ne convient pas pour afficher les objets de la liste imbriquée.

Je connais aussi @JsonUnwrapped, mais il n’est pas conçu pour être utilisé avec des listes.

Articles Similaires

Articles connexes (désérialisation)

Bibliothèque connexe

8
JJD

Vous pouvez utiliser la variable BeanSerializerModifier pour modifier directement la manière dont le nom et la valeur d'une propriété sont écrits. En utilisant cela, vous pourriez détecter si une annotation personnalisée est présente. Dans ce cas, j'ai créé une annotation appelée @FlattenCollection. Lorsque l'annotation est présente, le tableau ou la collection n'est pas écrit à l'aide de la méthode normale, mais à la place par un auteur de propriété personnalisé (FlattenCollectionPropertyWriter). 

Cette annotation sera probablement brisée sur les tableaux 2D ou d'autres cas Edge, je n'ai pas testé ceux-ci, mais vous pouvez probablement les coder sans trop de peine, du moins une erreur significative.

Heres le code de travail complet. Les points notables sont 

  • FlattenCollectionSerializerModifier.changeProperties
  • FlattenCollectionPropertyWriter.serializeAsField
  • Le couple de TODO que j'ai mis là pour vous.

Sortie:

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

Code:

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;
        }
    }
}
3
ug_

Basé sur ce lien je soupçonne que l'annotation au niveau du champ ne concerne que les délégués écrivant la valeur et non des propriétés entières. 

Une solution (plutôt kludgey) pourrait consister à créer un sérialiseur personnalisé pour toute la classe City:

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

...et alors 

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

}
1
AjahnCharles