sdegroot
sdegroot

Reputation: 247

Jackson add custom field with hash of another field

So, the title basically describes what I need. Say, the bean to be serialized looks like this:

public class SomeBean {
    public String someString;
}

I would like Jackson to serialize an instance of SomeBean like this:

{
    someString: '<the value>',
    __hash_someString: '<a proprietary hash of <the value>>'
}

This functionality should be generic. I don't want to write a specific serializer for SomeBean because this needs to happen at multiple locations. It is not an option to add the '__hash_someString' to the class itself because it would pollute the model.

Implementation

I would like Jackson to process the bean normally. But when it encounters a specific annotation (@GenerateHash) it should add another field to the object like before. So it would like this:

public class SomeBean {
    @GenerateHash
    public String someString;
}

The road so far

There are a lot of similar topics but none of them attempt something like this. I'm not really into the inner workings of Jackson Serialization but it seems you only get the option of modifying an object as a whole. I haven't found a way to intercept the serialization process of a field, only the value of that field.

I've tried to implement this using a BeanSerializerModifier and also tried some things with @Serializer. However, I usually end up in a infinite loop.

Resources I consulted are (not limited to):

In short How can I get Jackson to serialize

public class SomeBean {
    @GenerateHash
    public String someString;

    public String unaffectedString;
}

to this:

{
    someString: '<the value>',
    __hash_someString: '<a proprietary hash of <the value>>',
    unaffectedString: '<some value>'
}  

Upvotes: 5

Views: 2800

Answers (1)

Alexey Gavrilov
Alexey Gavrilov

Reputation: 10853

This is quite interesting. I think you can solve this with BeanSerializerModifier.

The idea is to register a custom serialiser which would have access to the original bean serialiser, the property description and the object value. If the property is annotated with a GenerateHash annotation, then the serialiser will emit an additional field. Here is an example:

public class JacksonGenerateHash {
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface GenerateHash {
    }

    public static class Bean {
        @GenerateHash
        public final String value;

        public Bean(final String value) {
            this.value = value;
        }
    }

    private static class MyBeanSerializerModifier extends BeanSerializerModifier {
        @Override
        public JsonSerializer<?> modifySerializer(
                final SerializationConfig serializationConfig,
                final BeanDescription beanDescription,
                final JsonSerializer<?> jsonSerializer) {
            return new HashGeneratingSerializer((JsonSerializer<Object>) jsonSerializer, null);
        }
    }

    private static class HashGeneratingSerializer extends JsonSerializer<Object>
            implements ContextualSerializer {
        private final JsonSerializer<Object> serializer;
        private final BeanProperty property;

        public HashGeneratingSerializer(
                final JsonSerializer<Object> jsonSerializer,
                final BeanProperty property) {
            this.serializer  = jsonSerializer;
            this.property = property;
        }

        @Override
        public void serialize(
                final Object o,
                final JsonGenerator jsonGenerator,
                final SerializerProvider serializerProvider)
        throws IOException {
            serializer.serialize(o, jsonGenerator, serializerProvider);
            // if the generatehash is present the property must be set
            if (property != null) {
               jsonGenerator.writeNumberField("_hash_" + property.getName(), o.hashCode());
            }
        }
        // override this method to access the bean property
        @Override
        public JsonSerializer<?> createContextual(
                final SerializerProvider prov, final BeanProperty property)
                throws JsonMappingException {
            if (property != null && property.getAnnotation(GenerateHash.class) != null) {
                return new HashGeneratingSerializer(serializer, property);
            }
            return serializer;
        }
    }

    public static void main(String[] args) throws JsonProcessingException {
        SimpleModule module = new SimpleModule();
        module.setSerializerModifier(new MyBeanSerializerModifier());
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(module);
        System.out.println(mapper.writeValueAsString(new Bean("abc")));
    }
}

Output:

{"value":"abc","_hash_value":96354}

Upvotes: 6

Related Questions