4

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?
}
1
  • Can you provide sample JSON Output of what you are trying to achieve? Seem a bit complex to understand without the output part Commented Oct 1, 2018 at 17:20

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

Comments

0
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}}

4 Comments

Thanks for your answer. This certainly is a "brute force" solution which probably works, but I don't think it has any advantages over my proposed solution. It actually looks like it just introduces more overhead and complexity. Is there any advantage?
sorry, seems i missed some part of your question. So you are looking for type safity for field which supports custom serialization? Could you elaborate a bit more please
@Felk about advantages: more precision control over serialization/deserialization which doesn't conflict with Jackson API. So you can you Jackson annotations on both source and destination objects, you can easily introduce type safety
about overhead: since you are already working with custom serialization, i guess it's not an issue to write more efficient serializer which doesn't write more data than it's required. So it's matter of optimization.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.