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 NullSerializers are added into BeanPropertyWriters).)
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.