Reputation: 1637
I am looking to modify an object right before it gets serialized. I want to write a custom serializer to parse the object, then pass it to the default object serializer.
This is what I have:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
*
* @author Me
*/
public class PersonSerializer extends JsonSerializer<Person>{
@Override
public void serialize(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
//This returns a modified clone of Person value.
Person safePerson = PrivacyService.getSafePerson(value);
provider.defaultSerializeValue(safePerson, jgen);
}
}
But that just goes in an infinate loop. I have also tried:
provider.findTypedValueSerializer(Person.class, true, null).serialize(safePerson, jgen, provider);
That works, but it doesn't parse any of the fields in the object.
I also tried using a @JsonFilter
but it was extremely heavy and sextupled my load times.
Help! Thanks!
Upvotes: 11
Views: 13904
Reputation: 12070
Although I initially rejoiced over finding the @Nitroware's answer, it unfortunately does not work in Jackson 2.7.2 - BeanSerializerFactory.instance.createSerializer
introspects JsonSerializer
annotation on Person
class again, which leads to infinite recursion and StackOverflowError
.
The point where default serializer would be created if @JsonSerializer
were absent on POJO class is the BeanSerializerFactory.constructBeanSerializer
method. So let's just use this method directly. Since the method is protected
, we make it visible via factory subclass and feed it with information about serialized class. Also, we replace deprecated SimpleType.construct
method by its recommended replacement. Whole solution is:
public class PersonSerializer extends JsonSerializer<PersonSerializer> {
static class BeanSerializerFactoryWithVisibleConstructingMethod extends BeanSerializerFactory {
BeanSerializerFactoryWithVisibleConstructingMethod() {
super(BeanSerializerFactory.instance.getFactoryConfig());
}
@Override
public JsonSerializer<Object> constructBeanSerializer(SerializerProvider prov, BeanDescription beanDesc) throws JsonMappingException {
return super.constructBeanSerializer(prov, beanDesc);
}
}
private final BeanSerializerFactoryWithVisibleConstructingMethod defaultBeanSerializerFactory = new BeanSerializerFactoryWithVisibleConstructingMethod();
private final JavaType javaType = TypeFactory.defaultInstance().constructType(Person.class);
@Override
public void serialize(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Person safePerson = PrivacyService.getSafePerson(value);
JavaType type = TypeFactory.defaultInstance().constructType(Person.class);
BeanDescription beanDescription = provider.getConfig().introspect(type);
JsonSerializer<Object> defaultSerializer = defaultBeanSerializerFactory.constructBeanSerializer(provider, beanDescription);
defaultSerializer.serialize(safePerson, jgen, provider);
}
}
Unlike BeanSerializerModifier
-based solution where you are forced to declare and register special behaviour outside your custom serializer, with this solution special logic is still encapsulated in custom PersonSerializer
.
Eventually the logic might be pushed up to custom DefaultJsonSerializerAware
ancestor.
UPDATE 2017-09-28:
I found bug in reasoning stated above. Using sole BeanSerializerFactory.constructBeanSerializer
method is not enough. If original class contains null fields, they are not in output. (The reason is the constructBeanSerializer
method is indirectly called from createAndCacheUntypedSerializer
method which later calls addAndResolveNonTypedSerializer
method where NullSerializer
s are added into BeanPropertyWriter
s).)
Solution to this problem which seems correct to me and is quite simple is to reuse all serialization logic, not only constructBeanSerializer method. This logic starts in provider's serializeValue
method. The only inappropriate thing is custom JsonSerialize
annotation. So we redefine BeanSerializationFactory
to pretend the introspected class (and only it - otherwise JsonSerialize annotations on field types would not apply) has no JsonSerialize
annotation.
@Override
public void serialize(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Person safePerson = PrivacyService.getSafePerson(value);
ObjectMapper objectMapper = (ObjectMapper)jgen.getCodec();
Class<?> entityClass = value.getClass();
JavaType javaType = TypeFactory.defaultInstance().constructType(entityClass);
DefaultSerializerProvider.Impl defaultSerializerProvider = (DefaultSerializerProvider.Impl) objectMapper.getSerializerProviderInstance();
BeanSerializerFactory factoryIgnoringCustomSerializerOnRootClass = new BeanSerializerFactory(BeanSerializerFactory.instance.getFactoryConfig()) {
@Override
protected JsonSerializer<Object> findSerializerFromAnnotation(SerializerProvider prov, Annotated a) throws JsonMappingException {
JsonSerializer<Object> result = javaType.equals(a.getType()) ? null : super.findSerializerFromAnnotation(prov, a);
return result;
}
};
DefaultSerializerProvider.Impl updatedSerializerProvider = defaultSerializerProvider.createInstance(defaultSerializerProvider.getConfig(), factoryIgnoringCustomSerializerOnRootClass);
updatedSerializerProvider.serializeValue(jgen, value);
}
Note if you don't suffer with problem with nulls, previous solution is enough for you.
Upvotes: 6
Reputation: 259
Since Jackson 2.2 one might use the converter in JsonSerialize annotation:
@JsonSerialize(converter = OurConverter.class)
and the converter
public class OurConverter extends StdConverter<IN, OUT>
IN and OUT are same class if modifying object
Upvotes: 11
Reputation: 1637
Holy crap, after several hours of digging through this library, trying to write my own factory, and a thousand other things, I FINALLY got this stupid thing to do what I wanted:
public class PersonSerializer extends JsonSerializer<Person>{
@Override
public void serialize(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
Person safePerson = PrivacyService.getSafePerson(value);
//This is the crazy one-liner that will save someone a very long time
BeanSerializerFactory.instance.createSerializer(provider, SimpleType.construct(Person.class)).serialize(safePerson, jgen, provider);
}
}
Upvotes: 12