Tuesday, October 2, 2018

Deserialize string using custom deserializer specified in field of class

Leave a Comment

I need to write a method that takes some object, some field name fieldName that exists in the given object's class, and some field value value. The value is the JSON-serialized form of the field. That method shall take the value and deserialize it accordingly, something like this:

static void setField(Object obj, String fieldName, String value) throws Exception {     Field field = obj.getClass().getDeclaredField(fieldName)     Object valObj = objectMapper.readValue(value, field.getType());     field.set(obj, valObj); } 

(I actually only need to retrieve the deserialized value, and not set it again, but this makes it a better example.) This works, as long as jackson's default deserialization is sufficient. Now let's assume I have a class with a custom (de)serializer:

class SomeDTO {     String foo;     @JsonSerialize(using = CustomInstantSerializer.class)     @JsonDeserialize(using = CustomInstantDeserializer.class)     Instant bar; } 

One possible solution would be to manually check for JsonDeserialize annotations. However, I really do not want to try to replicate whatever policies Jackson follows to decide what serializer to use, as that seems brittle (for example globally registered serializers).

Is there a good way to deserialize the value using the field's deserialization configuration defined in the DTO class? Maybe deserializing the value into the field's type while passing the field's annotations along to Jackson, so they get honored?

I managed to get a hold of an AnnotatedMember instance, which holds all the required information (JSON-annotations and reflective field- or setter/getter-access), but couldn't figure out how I would use it to deserialize a standalone value due to lack of documentation:

final JavaType dtoType = objectMapper.getTypeFactory().constructType(SomeDTO.class); final BeanDescription description = objectMapper.getDeserializationConfig().introspect(dtoType); for (BeanPropertyDefinition propDef: beanDescription.findProperties()) {     final AnnotatedMember mutator = propertyDefinition.getNonConstructorMutator();     // now what? Also: How do I filter for the correct property? } 

2 Answers

Answers 1

One possibility would be to serialize the object, replace the given field, and then deserialize it again. This can be easily done when serializing from/to JsonNode instead of JSON-String, like this:

static Object setField(Object obj, String fieldName, String value) throws Exception {     // note: produces a new object instead of modifying the existing one     JsonNode node = objectMapper.valueToTree(obj);     ((ObjectNode) node).put(fieldName, value);     return objectMapper.readValue(node.traverse(), obj.getClass()); } 

However, serializing and deserializing a whole object just to deserialize a single field seems like a lot of overhead, and might be brittle because other aspects of the DTO class affect the deserialization process of the single field

Answers 2

import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer;  import java.io.IOException; import java.util.Map;  public final class Jackson {    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()       .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);    public static void main(String[] args) throws IOException {     Dto source = makeDto("Master", 31337);     Dto dst = makeDto("Slave", 0xDEADBEEF);      //1. read value of field "fieldName" from json source     //2. clones destination object, sets up field "fieldName" and returns it     //3. in case of no field either on "src" or "dst" - throws an exception     Object result = restoreValue(dst, "details", OBJECT_MAPPER.writeValueAsString(source));     System.out.println(result);   }    private static Object restoreValue(Object targetObject, String fieldName, String sourceObjectAsJson) throws IOException {     String targetObjectAsJson = OBJECT_MAPPER.writeValueAsString(targetObject);     Map sourceAsMap = OBJECT_MAPPER.readValue(sourceObjectAsJson, Map.class);     Map targetAsMap = OBJECT_MAPPER.readValue(targetObjectAsJson, Map.class);     targetAsMap.put(fieldName, sourceAsMap.get(fieldName));     String updatedTargetAsJson = OBJECT_MAPPER.writeValueAsString(targetAsMap);     return OBJECT_MAPPER.readValue(updatedTargetAsJson, targetObject.getClass());   }    private static Dto makeDto(String name, int magic) {     Dto dto = new Dto();     dto.setName(name);     CustomDetails details = new CustomDetails();     details.setMagic(magic);     dto.setDetails(details);     return dto;   }    private static final class Dto {     private String name;     @JsonSerialize(using = CustomDetails.CustomDetailsSerializer.class)     @JsonDeserialize(using = CustomDetails.CustomDetailsDeserializer.class)     private CustomDetails details;      public String getName() {       return name;     }      public void setName(String name) {       this.name = name;     }      public CustomDetails getDetails() {       return details;     }      public void setDetails(CustomDetails details) {       this.details = details;     }      @Override     public String toString() {       return "Dto{" +           "name='" + name + '\'' +           ", details=" + details +           '}';     }   }     private static final class CustomDetails {     private int magic;      public int getMagic() {       return magic;     }      public void setMagic(int magic) {       this.magic = magic;     }      @Override     public String toString() {       return "CustomDetails{" +           "magic=" + magic +           '}';     }      public static final class CustomDetailsSerializer extends StdSerializer<CustomDetails> {        public CustomDetailsSerializer() {         this(null);       }         public CustomDetailsSerializer(Class<CustomDetails> t) {         super(t);       }        @Override       public void serialize(CustomDetails details, JsonGenerator jg, SerializerProvider serializerProvider) throws IOException {         jg.writeStartObject();         jg.writeNumberField("_custom_property_magic", details.magic);         jg.writeEndObject();       }     }       private static final class CustomDetailsDeserializer extends StdDeserializer<CustomDetails> {        public CustomDetailsDeserializer() {         this(null);       }         public CustomDetailsDeserializer(Class<CustomDetails> t) {         super(t);       }        @Override       public CustomDetails deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {         JsonNode node = jp.getCodec().readTree(jp);         int magic = (Integer) node.get("_custom_property_magic").numberValue();         CustomDetails             customDetails = new CustomDetails();         customDetails.setMagic(magic);         return customDetails;       }     }   } } 

so the output is:

Dto{name='Slave', details=CustomDetails{magic=31337}} 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment