Search code examples
javajacksondeserialization

Java Static final instances hierarchy deserialization


I have 3 classes:

public class Base {
  private int code;
  private static final Map<String, Map<Integer, Base>> ALL_MAPS = 
  new HashMap<>();

 protected Base(int code) {
  this.code = code;
  getConstMap(this.getClass()).put(code, this);
}

protected Map<Integer, Base> getConstMap(Class<?> clazz) {
  Map<Integer, Base> res;
  if ((res = ALL_MAPS.get(clazz.getName())) == null) {
     res = new HashMap<>();
     ALL_MAPS.put(clazz.getName(), res);
  }
  return res;
}
//...
}

public class AConstants extends Base {
   public static final AConstants A_1 = new AConstants(1, 
 "sample1");
 public static final AConstants A_2 = new AConstants(2, "sample2");
   //...
   private final String text;
   private AConstants(int code, String text) {
      super(code);
      this.text = text;
   }
}

public class BConstants extends Base {
   public static final BConstants B_1 = new BConstants(1, 
"sample1", "moreText");
   public static final BConstants B_2 = new BConstants(2, 
"sample2", "moreText");
   //...
   private final String text;
   private final String moreText;
   private BConstants(int code, String text, String moreText) {
      super(code);
      this.text = text;
      this.moreText = moreText;
   }
}

What I want to achieve is having a base method for deserializing (and serealizing) static final instances in the Base class. Obviously, I can't do smth like this due to generic type being unable to be referenced from static context during compilation (plus I obviously can't get the class of CONST):

@JsonCreator
public static CONST deserialize(@JsonProperty("code") int code) {
    // Should get the class of CONST and retrieve static final instance by code
    return ALL_MAPS.get(CONST.class).get(code);
}

@JsonValue
public Integer serialize() {
    // Return code for static final instance of the necessary class
    return getKeyByValue(ALL_MAPS.get(CONST.class), this);
}
  

Is there a way to achieve this with Jackson? Or if not, how do I write a custom serializer/deserializer for this?


Solution

  • In case someone has a similiar use case, here's how I did it. Base class is basically a placeholder for all the static final instances (or should I call them constants) - ALL_MAPS contains key as the name of the class which extends Base and as of value - a Map of keys - code of the instance and the instance itself as value. When the subclass of Base is loaded into the JVM, all the constants in it (as they are static) are placed in the ALL_MAP. So when deserializing, apart from loading the class into the memory, I only need a class of the constant and a key to it (code). I can get the class in the custom deserializer just like this: https://github.com/FasterXML/jackson-databind/issues/2711

    After that, all is left is to load the class into memory - I achieved it via reflection by calling get method on any static field with null value as I don't want to allow creating instances of const subclasses at all. And then - to read the json node which contains the code and get the constant.

    As a note: on the Base class I used @JsonDeserialize(using = BaseConstDeserializer.class) where BaseConstDeserializer is the custom deserializer.

    Here is the code of the deserializer:

    public class BaseConstDeserializer<CONST extends BaseConst> extends StdDeserializer<CONST> implements ContextualDeserializer {
    private final Class<CONST> deserializedFieldClazz;
    
    protected BaseConstDeserializer(Class<CONST> deserializedFieldClazz) {
        super(BaseConst.class);
        this.deserializedFieldClazz = deserializedFieldClazz;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public CONST deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException {
        JsonNode node = jp.getCodec().readTree(jp);
        Integer code = JsonUtils.readSingleNode(node, "code", JsonNode::asInt, ValidUtils::isInteger);
        if (code != null) {
            BaseConst.initConst(deserializedFieldClazz);
            return (CONST) BaseConst.getConstAndClearMap(code, deserializedFieldClazz);
        }
        return null;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty property) throws JsonMappingException {
        Class<CONST> rawClass = (Class<CONST>) property.getType().getRawClass();
        return new BaseConstDeserializer<>(rawClass);
    }
    }
    

    The only drawback I can guess here is that I need to manage the memory usage by myself - by clearing the map every time I deserialized json. I know that in Spring I can use @Scope to set the "lifespan" of an instance of bean to the scope of request but I don't like creating and having those instances in this case at all. This is propably an unorthodox approach, but it serves my case and I like the solution. Anyway, if someone sees any other potentials cons and pros of it, I'd like to hear.