In a SpringBoot application I need to serialize and deserialize fields of a certain complex type as a primitive value, without using any Jackson's annotation on a field or accessor methods at all.
By default a bean would be searialized as a multi-level JSON structure. The goal is to represent it as a single-level object.
Default Single-level
{ {
id: 123, id: 123
name: "Vaclav Havel Airport Prague", => name: "Vaclav Havel Airport Prague",
city: { cityId: 456
id: 456, }
name: "Prague"
}
}
This means:
-
To implement a custom serialization and deserialization mechanism to turn an object from and into a primitive value.
-
Override Jackson's bean description properties so city field would be represented as cityId in a JSON string.
Serialization
By overriding BeanSerializerModifier.changeProperties()
method I was able to implement a hook that allows me to replace so-called property writers, responsible for a bean property serialization.
First thing I had to do is to create a custom property writer with a new property name "cityId". The property writer overrides it's method serializeAsField()
which is responsible for calling appropriate serializer. In my case serialization is straight forward so I can call JsonGenerator.writeNumberField()
directly without calling a serializer.
public class CustomBeanSerializerModifier extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); i++) {
BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
AnnotatedMember annotatedMember = beanPropertyWriter.getMember();
// Let's skip all fields that are not type of City
if (!City.class.isAssignableFrom(annotatedMember.getRawType())) {
continue;
}
// Replace original BeanPropertyWriter that serializes City object
// by custom writer that serialize only its id and change property's name.
String propertyName = beanPropertyWriter.getName() + "Id";
BeanPropertyWriter writer = new CustomBeanPropertyWriter(beanPropertyWriter, propertyName);
beanProperties.set(i, writer);
}
return super.changeProperties(config, beanDesc, beanProperties);
}
public static class CustomBeanPropertyWriter extends BeanPropertyWriter {
private CustomBeanPropertyWriter(BeanPropertyWriter base, String newSimpleName) {
super(base, base.getFullName().withSimpleName(newSimpleName));
}
@Override
public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
// In this simple case there is no need to work with serializer. Use the
// generator directly.
City city = (City) (_accessorMethod == null ? _field.get(bean) : _accessorMethod.invoke(bean));
gen.writeNumberField(getName(), city.getId());
}
}
}
Alternative approach for serialization would be to implement SimpleBeanPropertyFilter.serializeAsField()
method.
Deserialization
Similar to the serializer, a deserializer construction process can be intercepted by implementing methods of BeanDeserializerModifier
class.
With deserializer, things are a little bit more complicated. First, I had to change property's name in BeanDeserializerModifier.updateProperties()
method and then to modify deserializer builder in the BeanDeserializerModifier.updateBuilder()
method. Modifying property's name, directly in the builder does not work.
public class CustomBeanDeserializerModifier extends BeanDeserializerModifier {
@Override
public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config, BeanDescription beanDescription, List<BeanPropertyDefinition> propertyDefinitions) {
for (int i = 0; i < propertyDefinitions.size(); i++) {
BeanPropertyDefinition propertyDefinition = propertyDefinitions.get(i);
AnnotatedField annotatedField = propertyDefinition.getField();
if (!City.class.isAssignableFrom(annotatedField.getRawType())) {
continue;
}
// Original property definition is replace by a new instance with modified name.
String propertyName = propertyDefinition.getName() + "Id";
propertyDefinitions.set(i, propertyDefinition.withSimpleName(propertyName));
}
return super.updateProperties(config, beanDescription, propertyDefinitions);
}
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
for (Iterator<SettableBeanProperty> iterator = builder.getProperties(); iterator.hasNext(); ) {
SettableBeanProperty property = iterator.next();
Class<?> type = property.getType().getRawClass();
if (!City.class.isAssignableFrom(type)) {
continue;
}
// Custom serializer is attached to a new property instance
// and the original property is replaced by it.
JsonDeserializer<?> deserializer = new CustomDeserializer((Class<? extends City>) type);
property = property.withValueDeserializer(deserializer);
builder.addOrReplaceProperty(property, true);
}
return builder;
}
public static class CustomDeserializer extends StdDeserializer<City> {
private final Class<? extends City> type;
private IdOnlyModelDeserializer(Class<? extends BaseModel> type) {
super(type);
this.type = type;
}
@Override
public City deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// At this point it would make sense to get City object from repository.
long id = p.getLongValue();
City city = new City();
city.setId(id);
return city;
}
}
}
Modifiers registration
In SpringBoot application, custom modifiers can be registered via Jackson's module API and passed into ObjectMapper builder.
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
SimpleModule simpleModule = new SimpleModule()
.setSerializerModifier(new CustomBeanSerializerModifier())
.setDeserializerModifier(new CustomBeanDeserializerModifier());
builder.modules(simpleModule);
builder.build();