Jackson JSON Modify Object Before Serialization

10,546

Solution 1

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.

Solution 2

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

Solution 3

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

    }

}
Share:
10,546
Addo Solutions
Author by

Addo Solutions

Updated on July 22, 2022

Comments

  • Addo Solutions
    Addo Solutions almost 2 years

    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!