Jackson deserialize JSON into generic type using property name as value to fetch type from registry

Viewed 58

I have been trying to implement the deserialization of JSON into a generic type by using property value as a reference to what type to deserialize for T and fetch the type from the registry.

I was able to implement a proof of concept that I can deserialize generic type if I provide the correct JavaType by using TypeFactory.constructParametricType, but then I tried to implement JsonDeserializer but it ended up calling itself again because that deserializer is registered to Property.class so another thing I tried is to use new ObjectManager which worked, but I don't really want to create new ObjectMapper and would like to use the same one.

So maybe someone will be able to guide me in the correct direction. (I am aware of the annotations, but I can't really use them as the type registry requires specific logic)

Here is my current scratch code which works:

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Scratch {

  interface Type {

    @JsonIgnore
    public default String getName() {
      return this.getClass().getSimpleName();
    }
  }

  static class Property<T extends Type> {

    private String typeName;

    private T type;

    private String name;

    public Property() {

    }

    public Property(T type, String name) {
      this.typeName = type.getName();
      this.type = type;
      this.name = name;
    }

    public String getTypeName() {
      return typeName;
    }

    public void setTypeName(String typeName) {
      this.typeName = typeName;
    }

    public T getType() {
      return type;
    }

    public void setType(T type) {
      this.type = type;
      this.typeName = type.getName();
    }

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }

  static class TypeRegistry {

    private final Map<String, Class<? extends Type>> types = new HashMap<>();

    public void add(Type type) {
      types.put(type.getName(), type.getClass());
    }

    public boolean has(String typeName) {
      return types.containsKey(typeName);
    }

    public Class<? extends Type> get(String typeName) {
      return this.types.get(typeName);
    }
  }

  static class TextType implements Type {
    private String color;

    public String getColor() {
      return color;
    }

    public void setColor(String color) {
      this.color = color;
    }
  }

  static class NumberType implements Type {
    private String format;

    public String getFormat() {
      return format;
    }

    public void setFormat(String format) {
      this.format = format;
    }
  }


  public static void main(String[] args) throws Exception {
    var typeRegistry = new TypeRegistry();
    typeRegistry.add(new TextType());
    typeRegistry.add(new NumberType());

    var parameters = new ArrayList<Property<?>>();
    parameters.add(new Property<>(new TextType(), "text_field"));
    parameters.add(new Property<>(new NumberType(), "number_field"));

    var module = new SimpleModule();
    module.addDeserializer(Property.class, new JsonDeserializer<Property<?>>() {

      private final ObjectMapper newObjectMapper = new ObjectMapper();

      @Override
      public Property<?> deserialize(JsonParser p, DeserializationContext ctxt)
          throws IOException, JsonProcessingException {
        JsonNode node = p.getCodec().readTree(p);
        if (node == null || node.isNull()) {
          return null;
        }

        var typeName = node.get("typeName").asText();
        var type = typeRegistry.get(typeName);


        return newObjectMapper.readValue(
            node.toString(),
            ctxt.getTypeFactory().constructParametricType(Property.class, type)
        );
      }
    });

    var objectMapper = new ObjectMapper();

    var desObjectMapper = new ObjectMapper();
    desObjectMapper.registerModule(module);

    var jsonSingle = objectMapper.writeValueAsString(parameters.get(0));
    var json = objectMapper.writeValueAsString(parameters);

    var parsedSingle = objectMapper.readValue(
        jsonSingle,
        TypeFactory.defaultInstance().constructParametricType(
            Property.class,
            typeRegistry.get("TextType")
        )
    );

    var parsedProperties = desObjectMapper.readValue(
        json,
        new TypeReference<List<Property<?>>>() {}
    );
  }
}

Thanks for any help!

Edit 1 (Added @michael-gantman proposed solution):

var parsedProperties = objectMapper.readValue(
        json,
        new TypeReference<List<LinkedHashMap<String, Object>>>() {
        }
    );

    var castedProperties = new ArrayList<Property<?>>();
    for (var parsedProperty : parsedProperties) {
      var typeName = (String) parsedProperty.get("typeName");

      castedProperties.add(
          objectMapper.convertValue(
              parsedProperty,
              TypeFactory.defaultInstance().constructParametricType(
                  Property.class,
                  typeRegistry.get(typeName)
              )
          )
      );
    }
1 Answers

Here is just another approach. Deserialize your Json always into a Map<String, Object> (for JSON Object) or List<Object> (for Json List). And than convert your Map into your custom class based on your property. To do so you can have all your relevant classes have a constructor that accepts Map, or static method
T getInstance(Map<String, Object>) creates an instance. It would be simpler to implement Map -> particular class conversion than JSON -> particular class.

Related