Given the following POJOs ..
public class City {
private String title;
private List<Person> people;
}
...
public class Person {
private String name;
private int age;
}
I would like to let Jackson serialize instances of the classes to the following example JSON:
{
"title" : "New York",
"personName_1" : "Jane Doe",
"personAge_1" : 42,
"personName_2" : "John Doe",
"personAge_2" : 23
}
The JSON format is defined by an external API which I cannot change.
I already found that I can annotate the list field with a custom serializer such as:
@JsonSerialize(using = PeopleSerializer.class)
private List<Person> people;
... and here is a basic implementation I tried:
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;
}
}
However, this fails with an:
JsonGenerationException: Can not write a field name, expecting a value
I also looked into using Jackson's Converter interface but that's not suitable for unwrapping the nested list objects.
I am also aware of @JsonUnwrapped
but it is not designed to be used with lists.
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
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With