Skip to content

Spring boot: 3.4.3, Spring data Couchbase: 5.4.3 java.lang.IllegalArgumentException: Attribute of type java.util.Collections.SingletonList cannot be stored and must be converted. #2041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
umeshmishra099 opened this issue Mar 28, 2025 · 9 comments
Labels
status: feedback-provided Feedback has been provided

Comments

@umeshmishra099
Copy link

Hi
We are upgrading from spring boot 3.2.4 to 3.4.3 We are getting issue below issue.
java.lang.IllegalArgumentException: Attribute of type java.util.Collections.SingletonList cannot be stored and must be converted.

it was working Spring boot: [3.2.4]
demo_3_2.zip
it fails with Spring boot: [3.4.3]
demo_3_4.zip

Step to test

Run spring boot application
Run this curl
curl --location --request POST 'http://localhost:8080/api/testpojo/create'
--header 'Content-Type: application/json'
--data-raw '{
"field1": "value55",
"field2": [{
"locale": "en-US",
"value": "test data11"
}],
"field3": "value3",
"field4": "value4",
"field5": "value5"
}'
it fails for spring boot 3.4.3 with error java.lang.IllegalArgumentException: Attribute of type java.util.ArrayList cannot be stored and must be converted.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 28, 2025
@teetangh
Copy link

teetangh commented Apr 1, 2025

have created PR #2042

@mikereiche
Copy link
Collaborator

mikereiche commented Apr 1, 2025

Note to self: Need lombok 1.18.30 or later to use with JDK 21

There is a behavior change between 5.1.5 and 5.1.6. The only change between 5.1.5 and 5.1.6 is 2281ba2

https://github.com/spring-projects/spring-data-couchbase/commits/5.1.x?after=017e5eaae930ea13fdce4236e6f086682d5f7888

@mikereiche
Copy link
Collaborator

mikereiche commented Apr 1, 2025

@umeshmishra099 - the issue is that the mapping converter finds the JsonValue annotated method values() in LocalizedStrings - which returns a list. And it does not expect a non-Collection-like property to map to a collection. If the JsonValue annotation is removed - it seems to work.
I don't know why it does not go through that same path in 5.1.5 and earlier.

in 3.2, CachingPropertyValueConverterFactory, the delegate holds a SimpleConverterPropertyFactory
in 3.4, the delegate is a CouchbaseConverterPropertyFactory.

public <DV, SV, C extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, C> getConverter(PersistentProperty<?> property) {
  Optional<PropertyValueConverter<?, ?, ? extends ValueConversionContext<?>>> converter = this.cache.get(property);
  return converter != null ? (PropertyValueConverter)converter.orElse((Object)null) : this.cache.cache(property, this.delegate.getConverter(property));
}

The CouchbasePropertyValueConverter finds the @JsonValue annotated method to use for write conversion - that method returns a List which fails validation.

in 3.2, the delegate does not find any converter, thus the calling code in AbstractCouchbaseConverter.convertForWriteIfNeeded() continues to look for a converter, and finds the Local

	}
	if (processValueConverter && conversions.hasValueConverter(prop)) {
		return conversions.getPropertyValueConversions().getValueConverter(prop).write(value,
				new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
	}
	Class<?> targetClass = this.conversions.getCustomWriteTarget(value.getClass()).orElse(null);

@umeshmishra099
Copy link
Author

@mikereiche - LocalizedStrings is something thirdparty we are using. can you suggest any other way to fix it

@mikereiche
Copy link
Collaborator

mikereiche commented Apr 2, 2025

This is the commit that gives the different behavior. 2281ba2

Before that change, creating a MappingCouchbaseConverter( mappingContext ) does not have the converter for annotations (such as JsonValue), and therefore the LocalizedStringConverter was being used. And it was working because the LocalizedStringConverter returns a CouchbaseList, while the @JsonValue method of LocalizedStrings returns an Array. I don't know if/where it's possible to specify a priority when there are multiple options for conversions. Or if it's possible to recursively apply converters (the Array returned from the @JsonValue method would be converted to a CouchbaseList if ran through the converter)

After CustomMappingCouchbaseConverter is created, only CommonCustomConverterUtils.getCommonCustomConverters() are added.

    public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext,
                                                               CouchbaseCustomConversions couchbaseCustomConversions) {
        CustomMappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext);
        converter.setCustomConversions(new CouchbaseCustomConversions(CommonCustomConverterUtils.getCommonCustomConverters()));
        couchbaseCustomConversions.registerConvertersIn((ConverterRegistry) converter.getConversionService());
        return converter;
    }

The mapping converter should be created as the original in AbstractCouchbaseConfiguration, which takes the conversions as an argument.

@Bean
public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext,
		CouchbaseCustomConversions couchbaseCustomConversions) {
	MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey(), couchbaseCustomConversions);
	couchbaseMappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder());
	return converter;
}

Additional converters should be added by overriding AbstractCouchbaseConfiguration.additionalConverters(List converters). The type is Object to take either converters or converter factories.

@Override 
protected void additionalConverters(List<Object> converters){
    converters.addAll(CommonCustomConverterUtils.getCommonCustomConverters());
}

// use this constructor
public CustomMappingCouchbaseConverter (
MappingContext, CouchbasePersistentProperty> mappingContext,
CouchbaseCustomConversions conversions) {
super(mappingContext, null /* typeKey */, conversions);
}

notes to self:

reformat line 'converterConfigurationAdapter.registerConverters(newConverters);'

and this is the annotation converter factory

new CouchbasePropertyValueConverterFactory(null, AbstractCouchbaseConfiguration.annotationToConverterMap(), om));

@mikereiche
Copy link
Collaborator

What is the purpose of the read() method in CustomMappingCouchbaseConverter ? Could you not just use the provided mapper with the additional converters?

@mikereiche
Copy link
Collaborator

To fix:

Use these three methods in your CommonCouchbaseConfig...
mappingCouchbaseConverter uses your CustomMappingCouchbaseConverter (with the overridden read() method.
The LocalizedStrings converters are added in additionalConverters().
The annotation converter, which was causing the problem, is removed from customConversions().

    @Override
    @Bean
    public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext,
                                                               CouchbaseCustomConversions couchbaseCustomConversions) {
        CustomMappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext, couchbaseCustomConversions);
        couchbaseCustomConversions.registerConvertersIn((ConverterRegistry) converter.getConversionService());
        return converter;
    }

    @Override
    protected void additionalConverters(List<Object> converters){
        converters.addAll(CommonCustomConverterUtils.getCommonCustomConverters());
    }

    public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper) {
        List<Object> newConverters = new ArrayList();
        // The following
        newConverters.add(new OtherConverters.EnumToObject(getObjectMapper()));
        newConverters.add(new IntegerToEnumConverterFactory(getObjectMapper()));
        newConverters.add(new StringToEnumConverterFactory(getObjectMapper()));
        newConverters.add(new BooleanToEnumConverterFactory(getObjectMapper()));
        additionalConverters(newConverters);
        CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
            //SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
            //valueConversions.setConverterFactory(
            //  new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap(), objectMapper));
            // valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
            //valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
            //configurationAdapter.setPropertyValueConversions(valueConversions);
            configurationAdapter.registerConverters(newConverters);
        });
        return customConversions;
    }

and change the constructor of your CustomMappingCouchbaseConverter to use the supplied conversions, instead of the default ones from CouchbaseCustomConversions.

    public CustomMappingCouchbaseConverter (
      MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext,
    CouchbaseCustomConversions conversions) {
        super(mappingContext, null, conversions);
    }

@mikereiche mikereiche added status: feedback-provided Feedback has been provided and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 3, 2025
@umeshmishra099
Copy link
Author

@mikereiche it working fine after doing all changes you suggested.
closing the thread now.

@mikereiche
Copy link
Collaborator

Thanks for your patience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided
Projects
None yet
Development

No branches or pull requests

4 participants