How to serialize a List content to a flat JSON object with Jackson? How to serialize a List content to a flat JSON object with Jackson? json json

How to serialize a List content to a flat JSON object with Jackson?


You can use the BeanSerializerModifier to directly modify how a property name and value are written. Using this you could detect if a custom annotation is present, in this case I made one called @FlattenCollection. When the annotation is present the array or collection is not written using the normal method but instead written by a custom property writer (FlattenCollectionPropertyWriter).

This annotation will likely break on 2d arrays or other edge cases, I havent tested those but you could probably code for them without to much trouble, at least throw a meaningful error.

Heres the full working code. Notable points are

  • FlattenCollectionSerializerModifier.changeProperties
  • FlattenCollectionPropertyWriter.serializeAsField
  • The couple TODOs i put in there for you.

Output:

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


Based on this link I suspect the field-level annotation only delegates writing the value not entire properties.

A (rather kludgey) workaround might be to have a custom serializer for the entire City class:

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

...and then

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