Search code examples
javaspring-boothibernatespring-data-jpaspring-data

Can not store custom type into database as json string - java.lang.IllegalArgumentException: Can not set final field to java.util.LinkedHashMap


I have a problem using a custom type into my JPA entity and then convert and store it into the database as JSON string.

Here is the base JPA entity: Discount (We are using Lombok and builders for creating object instances. The issue occurs on the two fields of type LocalizedTexts):

@Value
@NoArgsConstructor(force = true)
@AllArgsConstructor
@Builder(builderMethodName = "internalBuilder")
@Entity
@Table(name="discount")
public class Discount {
    
    @Id
    @Column(name = "ID", nullable = false, unique = true)
    @Type(type="uuid-char")
    UUID id;
    
    @Column(name="NO", nullable = false, unique = true)
    String no;
    
    @Column(name="I_NO", nullable = false, unique = true)
    Integer iNo;
    
    @Convert(converter = JpaJsonConverter.class)
    @Column(name="DESIGNATION", nullable = false)
    LocalizedTexts designation;
    
    @Convert(converter = JpaJsonConverter.class)
    @Column(name="PRINT_TEXT")
    LocalizedTexts printText;
    
    ...

    /**
     * Builder with required parameters
     */
    public static Discount.DiscountBuilder builder(UUID id, String no, LocalizedTexts designation) {
        return internalBuilder()
                .id(id)
                .no(no)
                .designation(designation);
    }

    // Getters
    ...

}

I am using an Attribute converter to transform the custom type (extending a Map<Object, String):

@Converter
public class JpaJsonConverter implements AttributeConverter<Object, String> {

    private final static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Object localizedTexts) {
        String localizedTextsJson = null;
        try {
            localizedTextsJson = objectMapper.writeValueAsString(localizedTexts);
        } catch (JsonProcessingException ex) {
            //throw new RuntimeException();
        }
        return localizedTextsJson;
    }

    @Override
    public Object convertToEntityAttribute(String localizedTextsJSON) {
        Object localizedTexts = null;
        try {
            localizedTexts = objectMapper.readValue(localizedTextsJSON, Object.class);
        } catch (IOException ex) {
            //throw new RuntimeException();
        }
        return localizedTexts;
    }
}

And here is the custom type: LocalizedTexts (Map<Enum,String>):

public class LocalizedTexts extends HashMap<Language, String> implements EntityBase {
    public LocalizedTexts() {
    }

    public LocalizedTexts(Map map) {
        this.putAll(map);
    }

    public static LocalizedTextsBuilder internalBuilder() {
        return new LocalizedTextsBuilder();
    }

    public static class LocalizedTextsBuilder {
        LocalizedTextsBuilder() {
        }

        public LocalizedTexts build() {
            return new LocalizedTexts();
        }

        public String toString() {
            return "LocalizedTexts.LocalizedTextsBuilder()";
        }
    }
}

The issue occurs when I am trying to save a Discount object into the database:

...
discountRepository.save(discount);
...

I have checked and the attribute converter works as expected (transform LocalizedTexts into JSON strings). The full stack trace of the exception I get is the following:

org.springframework.orm.jpa.JpaSystemException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation; nested exception is org.hibernate.PropertyAccessException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation

    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:331)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
    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 jdk.proxy2/jdk.proxy2.$Proxy116.save(Unknown Source)
    at net.xxxxx.yyyyy.svc_voucher.messagehandler.DiscountHandler.handleCommand(DiscountHandler.java:58)
    at net.xxxxx.yyyyy.svc_voucher.handlers.TestDiscountHandler.Sending_script_to_messageHandler_handleCommand(TestDiscountHandler.java:109)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.hibernate.PropertyAccessException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation
    at org.hibernate.property.access.spi.SetterFieldImpl.set(SetterFieldImpl.java:72)
    at org.hibernate.tuple.entity.AbstractEntityTuplizer.setPropertyValues(AbstractEntityTuplizer.java:681)
    at org.hibernate.tuple.entity.PojoEntityTuplizer.setPropertyValues(PojoEntityTuplizer.java:144)
    at org.hibernate.persister.entity.AbstractEntityPersister.setPropertyValues(AbstractEntityPersister.java:5252)
    at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:498)
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:241)
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:318)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:172)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:70)
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:829)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:816)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311)
    at jdk.proxy2/jdk.proxy2.$Proxy113.merge(Unknown Source)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:650)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    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.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    ... 42 more
Caused by: java.lang.IllegalArgumentException: Can not set final net.xxxxx.yyyyy.entity.internal.LocalizedTexts field net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation to java.util.LinkedHashMap
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
    at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:83)
    at java.base/java.lang.reflect.Field.set(Field.java:799)
    at org.hibernate.property.access.spi.SetterFieldImpl.set(SetterFieldImpl.java:52)
    ... 81 more

Solution

  • At first it's tempting to believe that the final modifier is the cause of the issue, but in fact it's not.

    Let's try this simple test:

    import java.lang.reflect.Field;
    
    class Discount
    {
        private final Integer id = 0;
    }
    
    public class Test
    {
        public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
        {
            Discount d = new Discount();
            Field f = Discount.class.getDeclaredField("id");
            f.set(d, 1);
        }
    }
    

    We get:

    java.lang.IllegalAccessException: class Test cannot access a member of class Discount with modifiers "private final"
    

    This is not the error that you are getting (your stacktrace shows an IllegalArgumentException, not an IllegalAccessException).

    Let's change the code to add a setAccessible(true):

    import java.lang.reflect.Field;
    
    class Discount
    {
        private final Integer id = 0;
    }
    
    public class Test
    {
        public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
        {
            Discount d = new Discount();
            Field f = Discount.class.getDeclaredField("id");
            f.setAccessible(true);
            f.set(d, 1);
        }
    }
    

    This time it works, no exception is thrown.

    Now let's replace the 1 with "1":

    import java.lang.reflect.Field;
    
    class Discount
    {
        private final Integer id = 0;
    }
    
    public class Test
    {
        public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
        {
            Discount d = new Discount();
            Field f = Discount.class.getDeclaredField("id");
            f.setAccessible(true);
            f.set(d, "1");
        }
    }
    

    We get:

    java.lang.IllegalArgumentException: Can not set final java.lang.Integer field Discount.id to java.lang.String
    

    This time this is exactly the same error as yours. The IllegalArgumentException means that we're trying to assign an incompatible value (here, a String to an Integer).

    Your stacktrace says that you're trying to assign a LinkedHashMap to the designation field, which is incompatible indeed (designation being a LocalizedTexts).

    To understand where the LinkedHashMap comes from, let's take a look at your JpaJsonConverter. The method responsible for converting a JSON string to a LocalizedTexts object is:

    public Object convertToEntityAttribute(String localizedTextsJSON) {
        Object localizedTexts = null;
        try {
            localizedTexts = objectMapper.readValue(localizedTextsJSON, Object.class);
        } catch (IOException ex) {
            //throw new RuntimeException();
        }
        return localizedTexts;
    }
    

    You're calling readValue() with a JSON string containing an object, and Object.class as second argument. In such a case, Jackson returns a LinkedHashMap. It has no way to know that you want a LocalizedTexts.

    Here is a possible way to fix the method:

    @SuppressWarnings("unchecked")
    public Object convertToEntityAttribute(String localizedTextsJSON) {
        LocalizedTexts localizedTexts = new LocalizedTexts();
        try {
            Map<String,String> map = (Map<String,String>)objectMapper.readValue(localizedTextsJSON, Object.class);
            for(Map.Entry<String,String> e : map.entrySet())
                localizedTexts.put(Language.valueOf(e.getKey()), e.getValue());
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        return localizedTexts;
    }