Enum as map keys conversion not working in Spring Boot 2.4.1 and Elastic Search 7.10

Viewed 614

I was working with SpringBoot 2.2.6, Elastic Search 5.6.13 and I decided to upgrade to SpringBoot 2.4.1, ElasticSearch 7.10. I'm storing complex documents in ES, with Enums as values and map keys. For all the serialization between Enums and strings (plus custom serializers) I'm using a custom entity mapper with a custom configured Jackson ObjectMapper.

In ES 4, the ElasticsearchEntityMapper interface has been removed. And all conversions should be implemented as converters. So I implemented converters for each enum, and it writes correctly into ES.

But they are not being read correctly. I found several problems but I would like to start with one specific. The map keys are not being translated back to Enums. They are being read as string.

I debugged the Spring Boot code looking for a specific interface I should be implementing but I couldn't find any.

Any ideas?

https://docs.spring.io/spring-data/elasticsearch/docs/current-SNAPSHOT/reference/html/#elasticsearch-migration-guide-3.2-4.0.jackson-removal

UPDATE 2022-01: Spring Boot 2.6.x seems to be working correctly, allowing to use enums as map keys...

2 Answers

I've managed to solve this by writing custom MappingElasticsearchConverter

public class CustomMappingElasticsearchConverter extends MappingElasticsearchConverter {

    public CustomMappingElasticsearchConverter(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
        super(mappingContext);
    }

    @Override
    @Nullable
    protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property, TypeInformation<R> targetType) {
        R r = super.readValue(source, property, targetType);
        return convertMapKeysToEnum(r, targetType);
    }

    @SuppressWarnings("unchecked")
    private <R> R convertMapKeysToEnum(R value, TypeInformation<R> targetType) {
        if (!(value instanceof Map)) return value;
        Class<?> keyClass = targetType.getTypeArguments().get(0).getType();
        if (!Enum.class.isAssignableFrom(keyClass)) return value;
        return (R) ((Map<?, ?>) value).entrySet().stream()
            .collect(toMap(e -> Enum.valueOf((Class<Enum>) keyClass, e.getKey().toString()), e -> e.getValue()));
    }
}

Then just replace the default bean with your custom implementation

public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {

    ...

    @Bean
    @Nonnull
    @Override
    public ElasticsearchConverter elasticsearchEntityMapper(SimpleElasticsearchMappingContext elasticsearchMappingContext) {
        MappingElasticsearchConverter elasticsearchConverter = new CustomMappingElasticsearchConverter(
            elasticsearchMappingContext);
        elasticsearchConverter.setConversions(elasticsearchCustomConversions());
        return elasticsearchConverter;
    }
}

After some "trial and error" process I came up with a solution. You might find it as a workaround, but I couldn't find a better solution. Basically, you should register a custom Converter for your property and, within it, using ObjectMapper.convertValue() convert from the Map<String, Object> into your Map<MyEnum, MyType>. Here is an example. Of course, despite of creating a fresh instace of ObjectMapper each time, you may want to inject the instance from Spring:

public class RestClientConfig extends AbstractElasticsearchConfiguration {

@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
    return new ElasticsearchCustomConversions(List.of(
            MapToTypeMap1.INSTANCE
    ));
}

@ReadingConverter
enum MapToTypeMap1 implements Converter<Map<String, Object>, Map<MyEnum, MyType>> {

    INSTANCE;

    @Override
    public Map<MyEnum, MyType> convert(Map<String, Object> source) {
        final ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().build();
        final TypeFactory typeFactory = mapper.getTypeFactory();
        final JavaType javaType = typeFactory.constructParametricType(Map.class, MyEnum.class, MyType.class);
        return mapper.convertValue(source, javaType);
    }
}
Related