skip to Main Content

I need to serialize and deserialize in JSON format a Java map using the Jackson library.
The map type is Map<Enum<?>, Object> and its purpose is to store a configuration made by key-value pairs, where keys are enum instances coming from a third party library (hence I do not have control over their definition).
Keys can be enums of different type, whereas values can be int, double or String.
Also, you can assume that different enums have different instance names.

Serialization works fine:

public class Main {
  public enum IntParam {I1, I2, I3}
  public enum DoubleParam {D1, D2, D3}
  public enum StringParam {S1, S2, S3}

  public static void main(final String[] args) {
    Map<Enum<?>, Object> config = Map.of(
        IntParam.I1, 0, 
        DoubleParam.D2, 1.0, 
        StringParam.S3, "sss"
    );
    new ObjectMapper().writeValueAsString(config)
    // {"I1": 1, "D2": 1.0, "S3": "sss"} 
  }

The problem is deserializing the above object:

    String json = "{"I1": 1, "D2": 1.0, "S3": "sss"}";
    new ObjectMapper().readValue(json, new TypeReference<Map<Enum<?>, Object>>())

fails because Jackson cannot know which enum contains instances named I1, D2 and S3.

I would like to achieve something like

{
   "com.external.company.IntParam.I1": 1,
   "com.external.company.DoubleParam.D2": 1.0,
   "com.external.company.StringParam.S3": "sss"
}

during serialization and then be able to deserialize it.

2

Answers


  1. Chosen as BEST ANSWER

    I solved the problem writing a custom deserializer that loads the JSON object in a HashMap<String, Object> and then loops through the different enum types looking for the enum instance with the same name as the key in the map (this relies on the fact that all enum instances have different names across all the involved enum types).

    public class ConfigMapDeserializer extends JsonDeserializer<Map<Enum<?>, Object>> {
    
        @Override
        public Map<Enum<?>, Object> deserialize(
            final JsonParser jp, final DeserializationContext ctxt
        ) throws IOException, JsonProcessingException {
          final HashMap<String, Object> rawObject = jp.readValueAs(new TypeReference<HashMap<String, Object>>() {});
          final HashMap<Enum<?>, Object> config = new HashMap<>(rawObject.size());
    
          for (final Entry<String, Object> entry: rawObject.entrySet()) {
            final String name = entry.getKey();
    
            Enum<?> param = null;
            try { param = IntParam.valueOf(name); } catch (final Exception e) { /* no-op */ }
            try { param = DoubleParam.valueOf(name); } catch (final Exception e) { /* no-op */ }
            try { param = StringParam.valueOf(name); } catch (final Exception e) { /* no-op */ }
    
            if (param == null) {
              // DO SOMETHING...
            } else {
              config.put(param, entry.getValue());
            }
          }
    
          return config;
        }
      }
    

    Finally, I decorated the config property with

    @JsonDeserialize(using=ConfigMapDeserializer.class) Map<Enum<?>, Object> config;
    

  2. An enum and a map don’t seem like the correct tools here (too many problems), I would try to redesign, if at all possible. Still, this can be achieved with wrapper class and custom serializer/deserializer for keys – applying mixin on Enum to achieve polymorphic serialization does not seem possible (do not take my word for it though, I may be wrong).

    A wrapper for the enum:

    @JsonSerialize(keyUsing = EnumWrapperKeySerializer.class)
    @JsonDeserialize(keyUsing = EnumWrapperKeyDeserializer.class)
    public record EnumWrapper(Enum<?> anEnum) {
    }
    

    Notice keyUsing, that’s to make the de/serializers used specifically for map keys.

    A deserializer to preserve information about the enum class and the instance:

    class EnumWrapperKeySerializer extends JsonSerializer<EnumWrapper> {
    
      @Override
      public void serialize(EnumWrapper value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeFieldName(value.anEnum().getClass().getName() + "." + value.anEnum().name());
      }
    }
    

    A deserializer to parse to correct enum and instance of that enum:

    public class EnumWrapperKeyDeserializer extends KeyDeserializer {
    
      @Override
      public Object deserializeKey(String key, DeserializationContext ctxt) {
        int index = key.lastIndexOf(".");
        String enumName = key.substring(0, index);
        Class<? extends Enum> enumClass;
        try {
          enumClass = (Class<? extends Enum>) Class.forName(enumName);
        } catch (ClassNotFoundException e) {
          //probably should not happen
          throw new RuntimeException(e);
        }
        String enumValue = key.substring(index + 1);
        return new EnumWrapper(Enum.valueOf(enumClass, enumValue));
      }
    }
    

    Example:

    Map<EnumWrapper, Object> map = Map.of(
                new EnumWrapper(IntParam.I1), 1,
                new EnumWrapper(DoubleParam.D2), 1.0,
                new EnumWrapper(StringParam.S3), "sss");
    ObjectMapper mapper = new ObjectMapper();
    
    String res = mapper.writeValueAsString(map);
    System.out.println(res);
    Map<EnumWrapper, Object> map2 = mapper.readValue(res, new TypeReference<>() {
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search