1

I'm using spring data elasticsearch to search elasticsearch(7.12.1) documents:

  • spring-boot-starter-data-elasticsearch: 2.7.0, which uses
  • spring-data-elasticsearch: 4.4.0

and find spring data elasticsearch can deserialize array field of basic type correctly but not custom object.

Here comes the issue--

A simple Person index with two fields may be array, one is basic string hobbies and the other is custom object children(there is no need to use nested object for me, plain object is enough):

@Data
@Builder
@Document(indexName = "person", writeTypeHint = WriteTypeHint.FALSE, createIndex = true)
public class Person {

    @Id
    @ReadOnlyProperty
    private String id;

    @Field(type = FieldType.Keyword)
    private Set<String> hobbies;

    @Field(type = FieldType.Object)
    private Set<Child> children;

    @Data
    public static class Child {
        @Field(type = FieldType.Keyword)
        private String name;
    }
}

it creates mappings automatically as expected:

{
  "person" : {
    "mappings" : {
      "properties" : {
        "children" : {
          "properties" : {
            "name" : {
              "type" : "keyword"
            }
          }
        },
        "hobbies" : {
          "type" : "keyword"
        }
      }
    }
  }
}

put 4 docs manually now:

PUT person/_doc/one-hobby
{
  "hobbies": "one"
}

PUT person/_doc/two-hobbies
{
  "hobbies": ["one", "two"]
}
PUT person/_doc/one-child
{
  "children": {"name": "name1"}
}

PUT person/_doc/two-children
{
  "children": [{"name": "name1"}, {"name": "name2"}]
}

Then query them one by one by their ids using ElasticsearchRepository#findById(id), the two docs with array values deserialize ok:

Person(id=two-children, hobbies=null, children=[Person.Child(name=name1), Person.Child(name=name2)])
Person(id=two-hobbies, hobbies=[one, two], children=null)

This is understandable because convert arrays to Set in java seems natural.

the one with only a basic field deserializes is ok too:

Person(id=one-hobby, hobbies=[one], children=null)

Although it convers a single string to Set, I've seen some of the deserializing source code, spring-data-elasticsearch uses string to collection converter to convert the single value hobbies into arraied-one, thus hobbies field in java as a Set behaves well.

but the one with only a custom object field(id=one-child) can't be deserialized:

Exception in thread "main" org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate java.util.Set using constructor NO_CONSTRUCTOR with arguments 
    at org.springframework.data.mapping.model.ReflectionEntityInstantiator.instantiateClass(ReflectionEntityInstantiator.java:116)
    at org.springframework.data.mapping.model.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:53)
    at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:102)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.readEntity(MappingElasticsearchConverter.java:320)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.read(MappingElasticsearchConverter.java:258)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.readValue(MappingElasticsearchConverter.java:458)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.readValue(MappingElasticsearchConverter.java:442)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader$ElasticsearchPropertyValueProvider.getPropertyValue(MappingElasticsearchConverter.java:621)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader$ElasticsearchPropertyValueProvider.getPropertyValue(MappingElasticsearchConverter.java:601)
    at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:74)
    at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:53)
    at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.extractInvocationArguments(ClassGeneratingEntityInstantiator.java:313)
    at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:285)
    at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:102)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.readEntity(MappingElasticsearchConverter.java:320)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.read(MappingElasticsearchConverter.java:258)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter$Reader.read(MappingElasticsearchConverter.java:217)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.read(MappingElasticsearchConverter.java:161)
    at org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter.read(MappingElasticsearchConverter.java:83)
    at org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate$ReadDocumentCallback.doWith(AbstractElasticsearchTemplate.java:745)
    at org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate.get(ElasticsearchRestTemplate.java:207)
    at org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository.lambda$findById$0(SimpleElasticsearchRepository.java:102)
    at org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository.execute(SimpleElasticsearchRepository.java:355)
    at org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository.findById(SimpleElasticsearchRepository.java:102)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    at com.sun.proxy.$Proxy62.findById(Unknown Source)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    at com.sun.proxy.$Proxy62.findById(Unknown Source)
    at io.puppylpg.PersonOps.get(PersonOps.java:23)
    at io.puppylpg.App.main(App.java:43)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.util.Set]: Specified class is an interface
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:143)
    at org.springframework.data.mapping.model.ReflectionEntityInstantiator.instantiateClass(ReflectionEntityInstantiator.java:112)
    ... 54 more

Seems that spring-data-elasticsearch won't try to convert custom object into Set because in MappingElasticsearchConverter if value is a map(a custom object), it just tries to read it directly, thus doesn't go the last line getPotentiallyConvertedSimpleRead(thus doesn't use some kine of custom object to collection converter like before):

        private <T> T readValue(Object value, TypeInformation<?> type) {

            Class<?> rawType = type.getType();

            if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
                return (T) conversionService.convert(value, rawType);
            } else if (value instanceof List) {
                return (T) readCollectionOrArray(type, (List<Object>) value);
            } else if (value.getClass().isArray()) {
                return (T) readCollectionOrArray(type, Arrays.asList((Object[]) value));
            } else if (value instanceof Map) {
                return (T) read(type, (Map<String, Object>) value);
            } else {
                return (T) getPotentiallyConvertedSimpleRead(value, rawType);
            }
        }

Is this supported in spring data elasticsearch? And any ideas to fix this problem or work around it?

Any suggestion would be appreciated.

2
  • Does it work when using a List? If so, do you need a set instead of a list? Commented Aug 22, 2022 at 5:41
  • @P.J.Meisch same error, except that error info is changed to MappingInstantiationException: Failed to instantiate java.util.List using constructor NO_CONSTRUCTOR with arguments Commented Aug 22, 2022 at 6:34

1 Answer 1

2

Can youplease create an issue at https://github.com/spring-projects/spring-data-elasticsearch/issues, best with a sample repo that reproduces the error?

Update 29.08.2022: This has been fixed and will be released with the next maintenance releases of branches 4.4 and 4.3.

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

1 Comment

Sure, i planed to create a issue before asking here, but thinked it was such a common case that may have solution already. Now i think spring-data-elasticsearch should add some suppport for detecting "single object or array" when deserializing a field.

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.