(De-)Serialize Bean in a custom way at runtime

jackson, java, json

Solution

I think `@JsonSerialize` and `@JsonDeserialize` is what you need. These annotations give you control on the serialization/deserialization of particular fields. This question shows elegant way to combine them into one annotation.

UPD. For this complex scenario you could take a look at `BeanSerializerModifier/BeanDeserializerModifier` classes. The idea is to modify general `BeanSerializer/BeanDeserializer` with your custom logic for particular fields and let basic implementation to do other stuff. Will post an example some time later.

UPD2. As I see, one of the way could be to use `changeProperties` method and assign your own serializer.

UPD3. Updated with working example of custom serializer. Deserialization could be done in similar way.

UPD4. Updated example with full custom serialization/deserialization. (I have used jakson-mapper-asl-1.9.8)

  public class TestBeanSerializationModifiers {

    static final String PropertyName = "customProperty";
    static final String CustomValue = "customValue";
    static final String BaseValue = "baseValue";

    // Custom serialization

    static class CustomSerializer extends JsonSerializer<Object> {
        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
            String customValue = CustomValue; // someService.getCustomValue(value);
            jgen.writeString(customValue);
        }
    }

    static class MyBeanSerializerModifier extends BeanSerializerModifier {
        @Override
        public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BasicBeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
            for (int i = 0; i < beanProperties.size(); i++) {
                BeanPropertyWriter beanPropertyWriter = beanProperties.get(i);
                if (PropertyName.equals(beanPropertyWriter.getName())) {
                    beanProperties.set(i, beanPropertyWriter.withSerializer(new CustomSerializer()));
                }
            }
            return beanProperties;
        }
    }

    // Custom deserialization

    static class CustomDeserializer extends JsonDeserializer<Object> {
        @Override
        public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            // serialized value, 'customValue'
            String serializedValue = jp.getText();
            String baseValue = BaseValue; // someService.restoreOldValue(serializedValue);
            return baseValue;
        }
    }

    static class MyBeanDeserializerModifier extends BeanDeserializerModifier {
        @Override
        public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BasicBeanDescription beanDesc, BeanDeserializerBuilder builder) {
            Iterator<SettableBeanProperty> beanPropertyIterator = builder.getProperties();
            while (beanPropertyIterator.hasNext()) {
                SettableBeanProperty settableBeanProperty = beanPropertyIterator.next();
                if (PropertyName.equals(settableBeanProperty.getName())) {
                    SettableBeanProperty newSettableBeanProperty = settableBeanProperty.withValueDeserializer(new CustomDeserializer());
                    builder.addOrReplaceProperty(newSettableBeanProperty, true);
                    break;
                }
            }
            return builder;
        }
    }

    static class Model {

        private String customProperty = BaseValue;
        private String[] someArray = new String[]{"one", "two"};

        public String getCustomProperty() {
            return customProperty;
        }

        public void setCustomProperty(String customProperty) {
            this.customProperty = customProperty;
        }

        public String[] getSomeArray() {
            return someArray;
        }

        public void setSomeArray(String[] someArray) {
            this.someArray = someArray;
        }
    }

    public static void main(String[] args) {
        SerializerFactory serializerFactory = BeanSerializerFactory
                .instance
                .withSerializerModifier(new MyBeanSerializerModifier());

        DeserializerFactory deserializerFactory = BeanDeserializerFactory
                .instance
                .withDeserializerModifier(new MyBeanDeserializerModifier());

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializerFactory(serializerFactory);
        objectMapper.setDeserializerProvider(new StdDeserializerProvider(deserializerFactory));

        try {
            final String fileName = "test-serialization.json";
            // Store, "customValue" -> json
            objectMapper.writeValue(new File(fileName), new Model());
            // Restore, "baseValue" -> model
            Model model = objectMapper.readValue(new File(fileName), Model.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Problem

Let's imagine I have the following POJO: ``` class Pojo { String s; Object o; Map<String, String> m; } ``` And at runtime, I want default serialization / deserialization for all properties except one. Typically, I want to replace a field by its ID in a database when serializing, similarly to this other question. For example, I want to replace `o` by a string obtained from an external mapping (for example: `object1` <=> "123" and `object2` <=> "456"): - serialization: read `o` and replace (so if `o` is `object1`, serialize as string "123") - deserialization: read "123", query some table to get the original value of `o` back (i.e. `object1`), recreate a `Pojo` object with `o = object1`. I understand that Modules would be one way to do that but I'm not sure how to use them while keeping the automatic BeanSerializer/Deserializer for the properties that don't need to be changed. Can someone give an example (even contrived) or an alternative approach? Notes: - I can't use annotations or Mixins as the changes are unknown at compile time (i.e. any properties might be changed in a way that is not determinable). - This other question points to using a CustomSerializerFactory, which seems to do the job. Unfortunately, the official site indicates that it is not the recommended approach any more and that modules should be used instead. Edit To be a little clearer, I can do the following with Mixins for example: ``` ObjectMapper mapper = new ObjectMapper(MongoBsonFactory.createFactory()); mapper.addMixInAnnotations(Pojo.class, PojoMixIn.class); ObjectReader reader = mapper.reader(Pojo.class); DBEncoder dbEncoder = DefaultDBEncoder.FACTORY.create(); OutputBuffer buffer = new BasicOutputBuffer(); dbEncoder.writeObject(buffer, o); ``` with the following Mixin: ``` abstract class PojoMixIn { @JsonIgnore Object o; } ``` And then add the required string to the JSON content. But I would need to know at compile time that it is the `o` field that needs to be replaced, which I don't.

Original source

Related problems