Jackson custom deserialization for polymorphic objects - java

I looked in many questions related to this but i can't find this exact use case.
Assumption here is that i am using Java Jackson library.
I have the following class hierarchy:
public class Event {
#JsonProperty("si")
String sessionId;
#JsonProperty("eventType")
String eventType;
...
}
#JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class InitEvent extends Event
{
#JsonProperty("pm")
Params params;
public Params getParams()
{
return params;
}
.....
}
#JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class RecoEvent extends Event
{
#JsonProperty("ti")
String targetId;
#JsonProperty("tt")
int targetType;
public String getTargetId()
{
return targetId;
}
....
}
The rule of deserialization is:
If eventType == 0 then deserialize to a InitEvent
If eventType == 0 then deserialize to a RecoEvent
Out of the box the Jackson deserialization will not work because it does not know which class to deserialize with. One way to handle that is by doing as follows on the base class:
#JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="#class")
#JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class Event
The problem of this solution is assumes the client will serialize with the same mapper since the #class element needs to be present now in the JSON.
My client will not send the extra #class element in the incoming JSON.
What is the required solution?
How I could write a custom deserializer that picks the right derived class based on the eventType value?
Thx in advance

At the end I had to write my own deserializer.
This works in 2 steps, first implement the custom deserializer and then register it.
The implementation works as follows:
public class EvtListDeserializer extends JsonDeserializer<EventList>
{
private static ObjectMapper mapper = new ObjectMapper();
#Override
public EventList deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException
{
EventList eventList = new EventList();
List<Event> listOfEvents = new ArrayList<Event>();
ObjectCodec oc = jsonParser.getCodec();
JsonNode eventListNode = oc.readTree(jsonParser);
ArrayNode node = (ArrayNode) eventListNode.get("evt");
Iterator<JsonNode> events = node.elements();
for (; events.hasNext(); )
{
JsonNode eventNode = events.next();
int eventType = eventNode.get("type").asInt();
Event event;
if (eventType == EventTypes.MoveEvent.getValue())
{
event = mapper.readValue(eventNode.toString(), MoveEvent.class);
}
else if (eventType == EventTypes.CopyEvent.getValue())
{
event = mapper.readValue(eventNode.toString(), CopyEvent.class);
}
else
{
throw new InvalidEventTypeException("Invalid event type:" + eventType);
}
listOfEvents.add(event);
}
eventList.setEvents(listOfEvents);
return eventList;
}
}
Then you just register it as follows in the class that uses your mapper:
public void init()
{
SimpleModule usageModule = new SimpleModule().addDeserializer(EventList.class, new EvtListDeserializer());
mapper.registerModule(usageModule);
}
That works perfectly.

Related

Jackson Deserialization not calling deserialize on Custom Deserializer

I want to deserialize classes of the form:
public class TestFieldEncryptedMessage implements ITextMessage {
#JsonProperty("text")
#Encrypted(cipherAlias = "testAlias")
private String text;
public TestFieldEncryptedMessage() {
}
#JsonCreator
public TestFieldEncryptedMessage(#JsonProperty("text") String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Where the text is encrypted and deserialization should unencrypt the value before rebuilding the TestFieldEncryptedMessage instance.
I am following an approach very similar to: https://github.com/codesqueak/jackson-json-crypto
That is, I am building a module extending SimpleModule:
public class CryptoModule extends SimpleModule {
public final static String GROUP_ID = "au.com.auspost.messaging";
public final static String ARTIFACT_ID = "jackson-json-crypto";
private EncryptedSerializerModifier serializerModifier;
private EncryptedDeserializerModifier deserializerModifier;
public CryptoModule() {
}
public CryptoModule addEncryptionService(final EncryptionService encryptionService) {
serializerModifier = new EncryptedSerializerModifier(encryptionService);
deserializerModifier = new EncryptedDeserializerModifier(encryptionService);
return this;
}
#Override
public String getModuleName() {
return ARTIFACT_ID;
}
#Override
public Version version() {
return new Version(major, minor, patch, null, GROUP_ID, ARTIFACT_ID);
}
#Override
public void setupModule(final SetupContext context) {
if ((null == serializerModifier) || (null == deserializerModifier))
throw new EncryptionException("Crypto module not initialised with an encryption service");
context.addBeanSerializerModifier(serializerModifier);
context.addBeanDeserializerModifier(deserializerModifier);
}
}
As you can see, two modifiers are set up: the EncryptedSerializerModifier works perfectly and is called by the ObjectMapper, but the deserializer behind the EncryptedDeserializerModifier is ignored.
As is seen in many other examples on SO such as here: How can I include raw JSON in an object using Jackson?, I set up the EncryptedDeserializerModifier with:
public class EncryptedDeserializerModifier extends BeanDeserializerModifier {
private final EncryptionService encryptionService;
private Map<String, SettableBeanProperty> properties = new HashMap<>();
public EncryptedDeserializerModifier(final EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
#Override
public BeanDeserializerBuilder updateBuilder(final DeserializationConfig config, final BeanDescription beanDescription, final BeanDeserializerBuilder builder) {
Encrypted annotation = beanDescription.getType().getRawClass().getAnnotation(Encrypted.class);
Iterator it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = (SettableBeanProperty) it.next();
if (null != p.getAnnotation(Encrypted.class)) {
JsonDeserializer<Object> current = p.getValueDeserializer();
properties.put(p.getName(), p);
builder.addOrReplaceProperty(p.withValueDeserializer(new EncryptedJsonDeserializer(encryptionService, current, p)), true);
}
}
return builder;
}
}
Finally, the EncryptedJsonDeserializer itself overrides the following:
#Override
public Object deserialize(final JsonParser parser, final DeserializationContext context) throws JsonMappingException {
JsonDeserializer<?> deserializer = baseDeserializer;
if (deserializer instanceof ContextualDeserializer) {
deserializer = ((ContextualDeserializer) deserializer).createContextual(context, property);
}
return service.decrypt(parser, deserializer, context, property != null ? property.getType() : type);
}
#Override
public JsonDeserializer<?> createContextual(final DeserializationContext context, final BeanProperty property) throws JsonMappingException {
JsonDeserializer<?> wrapped = context.findRootValueDeserializer(property.getType());
return new EncryptedJsonDeserializer(service, wrapped, property);
}
The createContextual() method is called, but the deserialize method is not called. The property throughout the execution is always the "text" property, so I seem to have the right context.
anyone know why the ObjectMapper doesn't find the right Deserializer?
EDIT added implements ITextMessage to decrypted class, which I thought was an unimportant detail, but turned out to be the cause of the issue.
I found the issue! If you look closely at the TestFieldEncryptedMessage class, whose text field is encrypted, you can see that it implements an interface. The interface is used so that the messages give some extra tooling for asserts in tests, however for deserialization, there is an unintended consequence. When the ObjectMapper is working its way through the json string, it tries, I think, to match a deserializer to a field inside ITextMessage, not to a field inside TestFieldEncryptedMessage, which is why the custom deserializer was not called (there is no text field in ITextMessage).
Once I stopped implementing ITextMessage, the custom deserializer was called.

Jackson Json deserialization of an object to a list

I'm consuming a web service using Spring's RestTemplate and deserializing with Jackson.
In my JSON response from the server, one of the fields can be either an object or a list. meaning it can be either "result": [{}] or "result": {}.
Is there a way to handle this kind of things by annotations on the type I'm deserializing to ? define the member as an array[] or List<> and insert a single object in case of the second example ?
Can I write a new HttpMessageConverter that will handle it ?
Since you are using Jackson I think what you need is JsonDeserializer class (javadoc).
You can implement it like this:
public class ListOrObjectGenericJsonDeserializer<T> extends JsonDeserializer<List<T>> {
private final Class<T> cls;
public ListOrObjectGenericJsonDeserializer() {
final ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
this.cls = (Class<T>) type.getActualTypeArguments()[0];
}
#Override
public List<T> deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final ObjectCodec objectCodec = p.getCodec();
final JsonNode listOrObjectNode = objectCodec.readTree(p);
final List<T> result = new ArrayList<T>();
if (listOrObjectNode.isArray()) {
for (JsonNode node : listOrObjectNode) {
result.add(objectCodec.treeToValue(node, cls));
}
} else {
result.add(objectCodec.treeToValue(listOrObjectNode, cls));
}
return result;
}
}
...
public class ListOrObjectResultItemJsonDeserializer extends ListOrObjectGenericJsonDeserializer<ResultItem> {}
Next you need to annotate your POJO field. Let's say you have classes like Result and ResultItem:
public class Result {
// here you add your custom deserializer so jackson will be able to use it
#JsonDeserialize(using = ListOrObjectResultItemJsonDeserializer.class)
private List<ResultItem> result;
public void setResult(final List<ResultItem> result) {
this.result = result;
}
public List<ResultItem> getResult() {
return result;
}
}
...
public class ResultItem {
private String value;
public String getValue() {
return value;
}
public void setValue(final String value) {
this.value = value;
}
}
Now you can check your deserializer:
// list of values
final String json1 = "{\"result\": [{\"value\": \"test\"}]}";
final Result result1 = new ObjectMapper().readValue(json1, Result.class);
// one value
final String json2 = "{\"result\": {\"value\": \"test\"}}";
final Result result2 = new ObjectMapper().readValue(json2, Result.class);
result1 and result2 contain the same value.
You can achieve what you want with a configuration flag in Jackson's ObjectMapper:
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.build();
Just set this ObjectMapper instance to your RestTemplate as explained in this answer, and in the class you are deserializing to, always use a collection, i.e. a List:
public class Response {
private List<Result> result;
// getter and setter
}

Handle Polymorphic with StdDeserializer Jackson 2.5

I have three classes which inherits from a super class (SensorData)
#JsonDeserialize(using = SensorDataDeserializer.class)
public abstract class SensorData {
}
public class HumiditySensorData extends SensorData {
}
public class LuminositySensorData extends SensorData {
}
public class TemperatureSensorData extends SensorData {
}
I want convert a json input into one of this classes depending on a parameter. I'm trying to use Jackson StdDeserializer and I create a custom deserializer
#Component
public class SensorDataDeserializer extends StdDeserializer<SensorData> {
private static final long serialVersionUID = 3625068688939160875L;
#Autowired
private SensorManager sensorManager;
private static final String discriminator = "name";
public SensorDataDeserializer() {
super(SensorData.class);
SpringBeanProvider.getInstance().autowireBean(this);
}
#Override
public SensorData deserialize(JsonParser parser,
DeserializationContext context) throws IOException,
JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(parser);
ObjectNode sensor = (ObjectNode) root.get("data");
String type = root.get(discriminator).asText();
Class<? extends SensorData> clazz = this.sensorManager
.getCachedSensorsMap().get(type).sensorDataClass();
if (clazz == null) {
// TODO should throw exception
return null;
}
return mapper.readValue(sensor.traverse(), clazz);
}
}
My problem is that when I determine the correct type to mapping the concrete class, the mapper call again to the custom StdDeserializer. So I need a way
to broke the cycle when I have the correct type. The stacktrace is the next one
java.lang.NullPointerException
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:38)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:1)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:3532)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1868)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:47)
at com.hp.psiot.mapping.SensorDataDeserializer.deserialize(SensorDataDeserializer.java:1)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3560)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2660)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:205)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:200)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters (AbstractMessageConverterMethodArgumentResolver.java:138)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:184)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:105)
An example of input
{
"name":"temperature",
"data": {
"value":20
}
}
I only include the stacktrace to show that the mapper is calling again to the deserializer. The reason for the nullPointerException is that when the second the ObjectMapper is called the input is
"value":20
So, An exception is threw because we don't have the information to determine the type and it doesn't check if the input is correct
I want to avoid using JsonSubTypes and JsonTypeInfo if it's posible.
Thanks in advance!
Partial solution
In my case the SensorData is wrapped in other class (ServiceData)
class ServiceData {
#JsonDeserialize(using = SensorDataDeserializer.class)
List<SensorData> sensors;
}
So, I get rid of JsonDeserializer in SensorData class and put it in the field avoiding the cycle. The solution isn't the best, but in my case it helps me. But in the case that the class isn't wrapped in another one we still have the same problem.
Note that if you have a Collection and you annotate with JsonDeserialize that field you have to handle all the collection. Here is the modification
in my case
#Component
public class SensorDataDeserializer extends StdDeserializer<List<SensorData>> {
private static final long serialVersionUID = 3625068688939160875L;
#Autowired
private SensorManager sensorManager;
private static final String discriminator = "name";
public SensorDataDeserializer() {
super(SensorData.class);
SpringBeanProvider.getInstance().autowireBean(this);
}
#Override
public List<SensorData> deserialize(JsonParser parser,
DeserializationContext context) throws IOException,
JsonProcessingException {
try {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
ArrayNode root = (ArrayNode) mapper.readTree(parser);
int size = root.size();
List<SensorData> sensors = new ArrayList<SensorData>();
for (int i = 0; i < size; ++i) {
ObjectNode sensorHead = (ObjectNode) root.get(i);
ObjectNode sensorData = (ObjectNode) sensorHead.get("data");
String tag = sensorHead.get(discriminator).asText();
Class<? extends SensorData> clazz = this.sensorManager
.getCachedSensorsMap().get(tag).sensorDataClass();
if (clazz == null) {
throw new InvalidJson("unbound sensor");
}
SensorData parsed = mapper.readValue(sensorData.traverse(),
clazz);
if (parsed == null) {
throw new InvalidJson("unbound sensor");
}
sensors.add(parsed);
}
return sensors;
} catch (Throwable e) {
throw new InvalidJson("invalid data");
}
}
}
Hope it helps someone :)
Why don't you just use #JsonTypeInfo? Polymorphic handling is the specific use case for it.
In this case, you would want to use something like:
#JsonTypeInfo(use=Id.NAME, include=As.PROPERTY, property="name")
#JsonSubTypes({ HumiditySensorData.class, ... }) // or register via mapper
public abstract class SensorData { ... }
#JsonTypeName("temperature")
public class TemperaratureSensorData extends SensorData {
public TemperaratureSensorData(#JsonProperty("data") JsonNode data) {
// extract pieces out
}
}
which would handle resolution from 'name' into sub-type, bind contents of 'data' as JsonNode (or, if you prefer can use Map or Object or whatever type matches).

Deserializing fails for a class implementing Collection with Jackson

I have the following JSON:
{
"item": [
{ "foo": 1 },
{ "foo": 2 }
]
}
This is basically an object that contains a collection of items.
So I made a class to deserialize that:
public class ItemList {
#JsonProperty("item")
List<Item> items;
// Getters, setters & co.
// ...
}
Everything is working nicely up to this point.
Now, To make my life easier somewhere else, I decided that it would be nice to be able to iterate on the ItemList object and let it implement the Collection interface.
So basically my class became:
public class ItemList implements Collection<Item>, Iterable<Item> {
#JsonProperty("item")
List<Item> items;
// Getters, setters & co.
// Generated all method delegates to items. For instance:
public Item get(int position) {
return items.get(position);
}
}
The implementation works properly and nicely. However, the deserialization now fails.
Looks like Jackson is getting confused:
com.fasterxml.jackson.databind.JsonMappingException: Can not
deserialize instance of com.example.ItemList out of START_OBJECT token
I have tried to add #JsonDeserialize(as=ItemList.class) but it did not do the trick.
What's the way to go?
Obviously it does not work because Jackson uses the standard collection deserialiser for Java collection types which knows nothing about ItemList properties.
It is possible to make it work but not in a very elegant way. You need to configure ObjectMapper to replace the default collection deserialiser on a bean deserialiser created manually for the corresponding type. I have written an example that does this in BeanDeserializerModifier for all the classes annotated with a custom annotation.
Note that I have to override ObjectMapper to get access to a protected method createDeserializationContext of ObjectMapper to create a proper deserialisation context since the bean modifier does not have access to it.
Here is the code:
public class JacksonCustomList {
public static final String JSON = "{\n" +
" \"item\": [\n" +
" { \"foo\": 1 },\n" +
" { \"foo\": 2 }\n" +
" ]\n" +
"} ";
#Retention(RetentionPolicy.RUNTIME)
public static #interface PreferBeanDeserializer {
}
public static class Item {
public int foo;
#Override
public String toString() {
return String.valueOf(foo);
}
}
#PreferBeanDeserializer
public static class ItemList extends ArrayList<Item> {
#JsonProperty("item")
public List<Item> items;
#Override
public String toString() {
return items.toString();
}
}
public static class Modifier extends BeanDeserializerModifier {
private final MyObjectMapper mapper;
public Modifier(final MyObjectMapper mapper) {
this.mapper = mapper;
}
#Override
public JsonDeserializer<?> modifyCollectionDeserializer(
final DeserializationConfig config,
final CollectionType type,
final BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
if (type.getRawClass().getAnnotation(PreferBeanDeserializer.class) != null) {
DeserializationContext context = mapper.createContext(config);
try {
return context.getFactory().createBeanDeserializer(context, type, beanDesc);
} catch (JsonMappingException e) {
throw new IllegalStateException(e);
}
}
return super.modifyCollectionDeserializer(config, type, beanDesc, deserializer);
}
}
public static class MyObjectMapper extends ObjectMapper {
public DeserializationContext createContext(final DeserializationConfig cfg) {
return super.createDeserializationContext(getDeserializationContext().getParser(), cfg);
}
}
public static void main(String[] args) throws IOException {
final MyObjectMapper mapper = new MyObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new Modifier(mapper));
mapper.registerModule(module);
System.out.println(mapper.readValue(JSON, ItemList.class));
}
}
If you consider the item property to be the root value, you can than change your ItemList class as follows, using the #JsonRootName annotation:
#JsonRootName("item")
public class ItemList implements Collection<Item>, Iterable<Item> {
private List<Item> items = new ArrayList<>();
public Item get(int position) {
return items.get(position);
}
// implemented methods deferring to delegate
// ...
}
If you then activate the UNWRAP_ROOT_VALUE deserialization feature, things work as expected:
String json = "{\"item\": [{\"foo\": 1}, {\"foo\": 2}]}";
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.reader(ItemList.class);
ItemList itemList = reader
.with(DeserializationFeature.UNWRAP_ROOT_VALUE)
.readValue(json);
Serialization works equally well, with the WRAP_ROOT_VALUE serialization feature enabled:
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer();
Item item1 = new Item();
item1.setFoo(1);
Item item2 = new Item();
item2.setFoo(2);
ItemList itemList = new ItemList();
itemList.add(item1);
itemList.add(item2);
String json = writer
.with(SerializationFeature.WRAP_ROOT_VALUE)
.writeValueAsString(itemList);
// json contains {"item":[{"foo":1},{"foo":2}]}
This solution will obviously not suffice if your ItemList contains additional properties (other than the actual list) that will also need to be serialized/deserialized.

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

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.
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();
}
}
}

Categories

Resources