From c1e81b7e90e0a166afea79a88658e616872e3122 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 4 Sep 2025 15:16:06 +0200 Subject: [PATCH 1/8] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 9a1889723d..40038b9eda 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..a41ddc5c48 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 595e5a4250..1d9f4998dc 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-GH-5037-SNAPSHOT ../pom.xml From aee03df382a28d151b96cbbb567e75ac0c26a488 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 4 Sep 2025 16:23:37 +0200 Subject: [PATCH 2/8] Remove defaulting for UUID and BigInteger/BigDecimal representations. We now align with the driver recommendation to not favor a particular representation for UUID or BigInteger/BigDecimal to avoid representation changes caused by upgrades to a newer Spring Data version. We expect an explicit configuration by applications. --- .../config/MongoConfigurationSupport.java | 2 - .../mongodb/core/MongoClientFactoryBean.java | 3 +- .../mongodb/core/convert/MongoConverters.java | 92 ++++++++++++++++++- .../core/convert/MongoCustomConversions.java | 81 +++++----------- .../core/mapping/MongoSimpleTypes.java | 29 +++--- .../AotPersonRepositoryIntegrationTests.java | 1 + .../MongoClientNamespaceTests-context.xml | 2 - .../config/MongoNamespaceTests-context.xml | 13 ++- ...rsonRepositoryIntegrationTests-context.xml | 10 +- ...ositoryIntegrationTests-infrastructure.xml | 2 - ...MongoNamespaceIntegrationTests-context.xml | 6 +- .../ROOT/pages/mongodb/mapping/mapping.adoc | 26 +++--- 12 files changed, 160 insertions(+), 107 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java index 60c3ca38cc..33b2e5c9e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java @@ -20,7 +20,6 @@ import java.util.HashSet; import java.util.Set; -import org.bson.UuidRepresentation; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -226,7 +225,6 @@ protected boolean autoIndexCreation() { protected MongoClientSettings mongoClientSettings() { MongoClientSettings.Builder builder = MongoClientSettings.builder(); - builder.uuidRepresentation(UuidRepresentation.STANDARD); configureClientSettings(builder); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java index 7c66396302..e9cb21e700 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java @@ -23,8 +23,8 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.bson.UuidRepresentation; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; @@ -162,7 +162,6 @@ protected MongoClientSettings computeClientSetting() { getOrDefault(port, "" + ServerAddress.defaultPort()))); Builder builder = MongoClientSettings.builder().applyConnectionString(connectionString); - builder.uuidRepresentation(UuidRepresentation.STANDARD); if (mongoClientSettings != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index 1fd45e1960..36a335cce0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -23,9 +23,14 @@ import java.net.URI; import java.net.URL; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; import java.util.Currency; +import java.util.Date; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -83,19 +88,67 @@ abstract class MongoConverters { private MongoConverters() {} /** - * Returns the converters to be registered. + * Returns the {@code Date} to UTC converters to be registered. * * @return - * @since 1.9 + * @since 5.0 */ - static Collection getConvertersToRegister() { + static Collection getDateToUtcConverters() { - List converters = new ArrayList<>(); + List converters = new ArrayList<>(3); + + converters.add(DateToUtcLocalDateConverter.INSTANCE); + converters.add(DateToUtcLocalTimeConverter.INSTANCE); + converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); + + return converters; + } + + /** + * Returns the {@code Decimal128} converters to be registered. + * + * @return + * @since 5.0 + */ + static Collection getBigNumberDecimal128Converters() { + + List converters = new ArrayList<>(3); converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); converters.add(BigIntegerToDecimal128Converter.INSTANCE); + return converters; + } + + /** + * Returns the {@code String} converters to be registered for {@link BigInteger} and {@link BigDecimal}. + * + * @return + * @since 5.0 + */ + static Collection getBigNumberStringConverters() { + + List converters = new ArrayList<>(4); + + converters.add(BigDecimalToStringConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converters.add(BigIntegerToStringConverter.INSTANCE); + converters.add(StringToBigIntegerConverter.INSTANCE); + + return converters; + } + + /** + * Returns the converters to be registered. + * + * @return + * @since 1.9 + */ + static Collection getConvertersToRegister() { + + List converters = new ArrayList<>(); + converters.add(URLToStringConverter.INSTANCE); converters.add(StringToURLConverter.INSTANCE); converters.add(DocumentToStringConverter.INSTANCE); @@ -630,4 +683,35 @@ public Instant convert(BsonTimestamp source) { return Instant.ofEpochSecond(source.getTime(), 0); } } + + @ReadingConverter + private enum DateToUtcLocalDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalDateTime convert(Date source) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC")); + } + } + + @ReadingConverter + private enum DateToUtcLocalTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalTime convert(Date source) { + return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime(); + } + } + + @ReadingConverter + private enum DateToUtcLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(Date source) { + return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index f7af94728a..a546191553 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core.convert; -import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -33,6 +32,7 @@ import java.util.function.Consumer; import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -42,14 +42,9 @@ import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterRegistrar; -import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.SimplePropertyValueConversions; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.mongodb.core.convert.MongoConverters.BigDecimalToStringConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.BigIntegerToStringConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigDecimalConverter; -import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.lang.Contract; @@ -68,7 +63,6 @@ */ public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions { - private static final StoreConversions STORE_CONVERSIONS; private static final List STORE_CONVERTERS; static { @@ -80,7 +74,6 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus converters.addAll(GeoConverters.getConvertersToRegister()); STORE_CONVERTERS = Collections.unmodifiableList(converters); - STORE_CONVERSIONS = StoreConversions.of(MongoSimpleTypes.HOLDER, STORE_CONVERTERS); } /** @@ -156,7 +149,8 @@ public static class MongoConverterConfigurationAdapter { * List of {@literal java.time} types having different representation when rendered via the native * {@link org.bson.codecs.Codec} than the Spring Data {@link Converter}. */ - private static final Set> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, LocalDateTime.class); + private static final Set> JAVA_DRIVER_TIME_SIMPLE_TYPES = Set.of(LocalDate.class, LocalTime.class, + LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128; @@ -326,6 +320,7 @@ public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation re this.bigDecimals = representation; return this; } + /** * Optionally set the {@link PropertyValueConversions} to be applied during mapping. *

@@ -375,72 +370,40 @@ ConverterConfiguration createConverterConfiguration() { svc.init(); } - List converters = new ArrayList<>(STORE_CONVERTERS.size() + 7); + List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); if (bigDecimals == BigDecimalRepresentation.STRING) { - - converters.add(BigDecimalToStringConverter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); - converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); + storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); } - if (!useNativeDriverJavaTimeCodecs) { - - converters.addAll(customConverters); - return new ConverterConfiguration(STORE_CONVERSIONS, converters, convertiblePair -> true, - this.propertyValueConversions); + if (bigDecimals == BigDecimalRepresentation.DECIMAL128) { + storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); } - /* - * We need to have those converters using UTC as the default ones would go on with the systemDefault. - */ - converters.add(DateToUtcLocalDateConverter.INSTANCE); - converters.add(DateToUtcLocalTimeConverter.INSTANCE); - converters.add(DateToUtcLocalDateTimeConverter.INSTANCE); - converters.addAll(STORE_CONVERTERS); + if (useNativeDriverJavaTimeCodecs) { - StoreConversions storeConversions = StoreConversions - .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), converters); + /* + * We need to have those converters using UTC as the default ones would go on with the systemDefault. + */ + storeConverters.addAll(MongoConverters.getDateToUtcConverters()); + storeConverters.addAll(STORE_CONVERTERS); - return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> { + StoreConversions storeConversions = StoreConversions + .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), storeConverters); - // Avoid default registrations + return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> { + + // Avoid default registrations return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); }, this.propertyValueConversions); - } - - @ReadingConverter - private enum DateToUtcLocalDateTimeConverter implements Converter { - - INSTANCE; - @Override - public LocalDateTime convert(Date source) { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC")); } - } - - @ReadingConverter - private enum DateToUtcLocalTimeConverter implements Converter { - INSTANCE; - @Override - public LocalTime convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime(); - } - } - - @ReadingConverter - private enum DateToUtcLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(Date source) { - return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate(); - } + storeConverters.addAll(STORE_CONVERTERS); + return new ConverterConfiguration(StoreConversions.of(MongoSimpleTypes.createSimpleTypeHolder(), storeConverters), + this.customConverters, convertiblePair -> true, this.propertyValueConversions); } private boolean hasDefaultPropertyValueConversions() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 6b4d9b9e9b..aeb79fde40 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -62,22 +62,27 @@ public abstract class MongoSimpleTypes { BsonTimestamp.class, Geometry.class, GeometryCollection.class, LineString.class, MultiLineString.class, MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class); - public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { + public static final SimpleTypeHolder HOLDER = createSimpleTypeHolder(); - @Override - public boolean isSimpleType(Class type) { + public static SimpleTypeHolder createSimpleTypeHolder() { - if (type.isEnum()) { - return true; - } + return new SimpleTypeHolder(MONGO_SIMPLE_TYPES, true) { - if (type.getName().startsWith("java.time")) { - return false; - } + @Override + public boolean isSimpleType(Class type) { + + if (type.isEnum()) { + return true; + } - return super.isSimpleType(type); - } - }; + if (type.getName().startsWith("java.time")) { + return false; + } + + return super.isSimpleType(type); + } + }; + } private MongoSimpleTypes() {} } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java index bac2bde266..87b9f04ae4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AotPersonRepositoryIntegrationTests.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml index 79e5ac40a0..fcf8a258da 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/config/MongoClientNamespaceTests-context.xml @@ -3,10 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:context="http://www.springframework.org/schema/context" - xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> - + @@ -26,8 +25,8 @@ - - @@ -56,9 +55,9 @@ - + - + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml index 8c6194f0a4..a0451bb97e 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-context.xml @@ -1,12 +1,20 @@ + + + + + + diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml index 81a7c261f9..c802ee7c36 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests-infrastructure.xml @@ -7,8 +7,6 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - - diff --git a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml index b70efb607c..2993ee1e46 100644 --- a/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml +++ b/spring-data-mongodb/src/test/resources/org/springframework/data/mongodb/repository/config/MongoNamespaceIntegrationTests-context.xml @@ -9,7 +9,11 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - + + + + + diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index 121c80d563..2056dfa20e 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -119,7 +119,7 @@ See xref:mongodb/mapping/custom-conversions.adoc[Custom Conversions - Overriding | native | `{"bin" : { "$binary" : "AQIDBA==", "$type" : "00" }}` -| `java.util.UUID` (Standard UUID) +| `java.util.UUID` (According to UuidRepresentation) | native | `{"uuid" : { "$binary" : "MEaf1CFQ6lSphaa3b9AtlA==", "$type" : "04" }}` @@ -164,13 +164,13 @@ calling `get()` before the actual conversion | `{"value" : "741" }` | `BigInteger` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741) }`, `{"value" : "741" }` | `BigDecimal` -| converter + -`NumberDecimal`, `String` +| native + +`NumberDecimal`, `String` (see `BigDecimalRepresentation`) | `{"value" : NumberDecimal(741.99) }`, `{"value" : "741.99" }` | `URL` @@ -200,25 +200,21 @@ calling `get()` before the actual conversion | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `Instant` + -(Joda, JSR310-BackPort) +(Java 8) | converter | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` | `LocalDate` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T00:00:00.000Z")}` | `LocalDateTime`, `LocalTime` + -(Joda, Java 8, JSR310-BackPort) -| converter / native (Java8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] -| `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` - -| `DateTime` (Joda) -| converter +(Java 8) +| converter / native (Java 8)footnote:[Uses UTC zone offset. Configure via xref:mongodb/mapping/mapping.adoc#mapping-configuration[MongoConverterConfigurationAdapter]] | `{"date" : ISODate("2019-11-12T23:00:00.809Z")}` -| `ZoneId` (Java 8, JSR310-BackPort) +| `ZoneId` (Java 8) | converter | `{"zoneId" : "ECT - Europe/Paris"}` From 79603e10ff67e85fe91627d741f16b19b0b45cc1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 5 Sep 2025 09:56:06 +0200 Subject: [PATCH 3/8] Add migration guide. --- src/main/antora/modules/ROOT/nav.adoc | 1 + .../migration-guide-4.x-to-5.x.adoc | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 6f2d1e2847..ac69429f33 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -3,6 +3,7 @@ ** xref:migration-guides.adoc[] *** xref:migration-guide/migration-guide-2.x-to-3.x.adoc[] *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] +*** xref:migration-guide/migration-guide-4.x-to-5.x.adoc[] * xref:mongodb.adoc[] ** xref:preface.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc new file mode 100644 index 0000000000..48e08cdd43 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -0,0 +1,38 @@ +[[mongodb.migration.4.x-5.x]] += Migration Guide from 4.x to 5.x + +Spring Data MongoDB 4.x requires the MongoDB Java Driver 5.5.x + +To learn more about driver versions please visit the https://www.mongodb.com/docs/drivers/java/sync/current/upgrade/[MongoDB Documentation]. + +== UUID Representation Changes + +Spring Data no longer defaults UUID settings via its configuration support classes, factory beans, nor XML namespace. + +In order to persist UUID values the `UuidRepresentation` hast to be set explicitly. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { + builder.uuidRepresentation(UuidRepresentation.STANDARD); + } + + // ... +} +---- + +XML:: ++ +[source,xml,indent=0,subs="verbatim,quotes",role="secondary"] +---- + + + +---- +====== From 174865024733ab7f175883f81880d095e9a83f00 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 5 Sep 2025 12:57:12 +0200 Subject: [PATCH 4/8] Require explicit BigInteger/BigDecimal conversion settings. --- .../mongodb/core/convert/MongoConverters.java | 5 +- .../core/convert/MongoCustomConversions.java | 26 ++-- .../core/MongoTemplateCollationTests.java | 2 + .../data/mongodb/core/MongoTemplateTests.java | 134 ++++++++++++------ .../MappingMongoConverterUnitTests.java | 31 +++- .../core/convert/QueryMapperUnitTests.java | 22 ++- .../test/util/MongoConverterConfigurer.java | 6 + .../migration-guide-4.x-to-5.x.adoc | 19 +++ 8 files changed, 189 insertions(+), 56 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index 36a335cce0..2a8c8042a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -132,9 +132,7 @@ static Collection getBigNumberStringConverters() { List converters = new ArrayList<>(4); converters.add(BigDecimalToStringConverter.INSTANCE); - converters.add(StringToBigDecimalConverter.INSTANCE); converters.add(BigIntegerToStringConverter.INSTANCE); - converters.add(StringToBigIntegerConverter.INSTANCE); return converters; } @@ -169,6 +167,9 @@ static Collection getConvertersToRegister() { converters.add(ListToVectorConverter.INSTANCE); converters.add(BinaryVectorToMongoVectorConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converters.add(StringToBigIntegerConverter.INSTANCE); + converters.add(reading(BsonUndefined.class, Object.class, it -> null)); converters.add(reading(String.class, URI.class, URI::create).andWriting(URI::toString)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index a546191553..b68c8189e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -31,8 +31,9 @@ import java.util.Set; import java.util.function.Consumer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -63,6 +64,7 @@ */ public class MongoCustomConversions extends org.springframework.data.convert.CustomConversions { + private static final Log LOGGER = LogFactory.getLog(MongoCustomConversions.class); private static final List STORE_CONVERTERS; static { @@ -153,7 +155,7 @@ public static class MongoConverterConfigurationAdapter { LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; - private BigDecimalRepresentation bigDecimals = BigDecimalRepresentation.DECIMAL128; + private @Nullable BigDecimalRepresentation bigDecimals; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); @@ -372,12 +374,14 @@ ConverterConfiguration createConverterConfiguration() { List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); - if (bigDecimals == BigDecimalRepresentation.STRING) { - storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); - } - - if (bigDecimals == BigDecimalRepresentation.DECIMAL128) { - storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + if (bigDecimals != null) { + switch (bigDecimals) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + } + } else if (LOGGER.isInfoEnabled()) { + LOGGER.info( + "No BigDecimal/BigInteger representation set. Choose [STRING] or [DECIMAL128] to store values in desired format."); } if (useNativeDriverJavaTimeCodecs) { @@ -395,9 +399,9 @@ ConverterConfiguration createConverterConfiguration() { // Avoid default registrations - return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) - || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); - }, this.propertyValueConversions); + return !JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType()) + || !Date.class.isAssignableFrom(convertiblePair.getTargetType()); + }, this.propertyValueConversions); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java index bbb3c0eaef..5707772fcf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Collation.Alternate; import org.springframework.data.mongodb.core.query.Collation.ComparisonLevel; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 362044f35e..9146321e06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -15,10 +15,14 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; -import static org.springframework.data.mongodb.core.query.Update.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.data.mongodb.core.query.Criteria.expr; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; @@ -28,17 +32,29 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.convert.ConversionFailedException; @@ -69,6 +85,7 @@ import org.springframework.data.mongodb.core.convert.LazyLoadingProxy; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; @@ -144,6 +161,12 @@ public class MongoTemplateTests { it.defaultDb(DB_NAME); }); + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + adapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); + }); + cfg.configureMappingContext(it -> { it.autocreateIndex(false); it.initialEntitySet(AuditablePerson.class); @@ -170,7 +193,10 @@ public class MongoTemplateTests { }); cfg.configureConversion(it -> { - it.customConverters(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE); + it.customConverters(adapter -> { + adapter.registerConverters(List.of(DateToDateTimeConverter.INSTANCE, DateTimeToDateConverter.INSTANCE)) + .bigDecimal(BigDecimalRepresentation.DECIMAL128); + }); }); cfg.configureMappingContext(it -> { @@ -732,7 +758,7 @@ public void testDistinct() { .containsExactlyInAnyOrder(person1.getName(), person2.getName()); assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", template.getCollectionName(MyPerson.class), MyPerson.class, String.class)) - .containsExactlyInAnyOrder(person1.getName(), person2.getName()); + .containsExactlyInAnyOrder(person1.getName(), person2.getName()); } @Test // DATAMONGO-1761 @@ -876,7 +902,7 @@ public void testUsingAnInQueryWithLongId() throws Exception { } @Test // DATAMONGO-602, GH-4920 - public void testUsingAnInQueryWithBigIntegerId() throws Exception { + public void testUsingAnInQueryWithBigIntegerId() { template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); @@ -887,6 +913,34 @@ public void testUsingAnInQueryWithBigIntegerId() throws Exception { assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> template.insert(p1)); } + @Test // GH-5037 + public void errorsIfNoBigNumberFormatDefined() { + + template = new MongoTestTemplate(cfg -> { + + cfg.configureDatabaseFactory(it -> { + + it.client(client); + it.defaultDb(DB_NAME); + }); + + cfg.configureConversion(it -> { + it.customConverters(adapter -> { + // no numeric conversion + }); + }); + + }); + + template.remove(new Query(), PersonWithIdPropertyOfTypeBigInteger.class); + + PersonWithIdPropertyOfTypeBigInteger p1 = new PersonWithIdPropertyOfTypeBigInteger(); + p1.setFirstName("Sven"); + p1.setAge(11); + p1.setId(new BigInteger("2666666666666666665")); + assertThatExceptionOfType(CodecConfigurationException.class).isThrownBy(() -> template.insert(p1)); + } + @Test public void testUsingAnInQueryWithPrimitiveIntId() throws Exception { @@ -2561,9 +2615,7 @@ public void shouldReadNestedProjection() { walter.address = new Address("spring", "data"); template.save(walter); - PersonPWA result = template.query(MyPerson.class) - .as(PersonPWA.class) - .matching(where("id").is(walter.id)) + PersonPWA result = template.query(MyPerson.class).as(PersonPWA.class).matching(where("id").is(walter.id)) .firstValue(); assertThat(result.getAddress().getCity()).isEqualTo("data"); @@ -2571,6 +2623,7 @@ public void shouldReadNestedProjection() { interface PersonPWA { String getName(); + AdressProjection getAddress(); } @@ -2823,7 +2876,7 @@ public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() { assertThat(template.getDb().getCollection("sample").countDocuments( new org.bson.Document("field", new org.bson.Document("$in", Arrays.asList("spring", "mongodb"))))) - .isEqualTo(0L); + .isEqualTo(0L); assertThat(template.getDb().getCollection("sample").countDocuments(new org.bson.Document("field", "data"))) .isEqualTo(1L); } @@ -3935,7 +3988,8 @@ void saveEntityWithDotInFieldName() { template.save(source); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "v1"); } @@ -3954,13 +4008,17 @@ void queryEntityWithDotInFieldNameUsingExpr() { template.save(source); template.save(source2); - WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname mapping - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))).firstValue(); + WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) // with property -> fieldname + // mapping + .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); loaded = template.query(WithFieldNameContainingDots.class) // using raw fieldname - .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))).firstValue(); + .matching( + expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("field.name.with.dots")).equalToValue("v1"))) + .firstValue(); assertThat(loaded).isEqualTo(source); } @@ -3975,20 +4033,20 @@ void updateEntityWithDotInFieldNameUsingAggregations() { template.save(source); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))) - .first(); + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)).apply(AggregationUpdate + .newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "changed")))).first(); - org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed"); - template.update(WithFieldNameContainingDots.class) - .matching(where("id").is(source.id)) - .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) + template.update(WithFieldNameContainingDots.class).matching(where("id").is(source.id)) + .apply(AggregationUpdate.newUpdate( + ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("field.name.with.dots", "changed-again")))) .first(); - raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); + raw = template.execute(WithFieldNameContainingDots.class, + collection -> collection.find(new org.bson.Document("_id", source.id)).first()); assertThat(raw).containsEntry("field.name.with.dots", "changed-again"); } @@ -4013,9 +4071,8 @@ void savesMapWithDotInKey() { org.bson.Document raw = template.execute(WithFieldNameContainingDots.class, collection -> collection.find(new org.bson.Document("_id", source.id)).first()); - assertThat(raw.get("mapValue", org.bson.Document.class)) - .containsEntry("k1", "v1") - .containsEntry("map.key.with.dot", "v2"); + assertThat(raw.get("mapValue", org.bson.Document.class)).containsEntry("k1", "v1").containsEntry("map.key.with.dot", + "v2"); } @Test // GH-4464 @@ -4031,16 +4088,13 @@ void readsMapWithDotInKey() { MongoTemplate template = new MongoTemplate(new SimpleMongoClientDatabaseFactory(client, DB_NAME), converter); Map sourceMap = Map.of("k1", "v1", "sourceMap.key.with.dot", "v2"); - template.execute(WithFieldNameContainingDots.class, - collection -> { - collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); - return null; - } - ); + template.execute(WithFieldNameContainingDots.class, collection -> { + collection.insertOne(new org.bson.Document("_id", "id-1").append("mapValue", sourceMap)); + return null; + }); WithFieldNameContainingDots loaded = template.query(WithFieldNameContainingDots.class) - .matching(where("id").is("id-1")) - .firstValue(); + .matching(where("id").is("id-1")).firstValue(); assertThat(loaded.mapValue).isEqualTo(sourceMap); } @@ -5015,8 +5069,7 @@ static class WithFieldNameContainingDots { String id; - @Field(value = "field.name.with.dots", nameType = Type.KEY) - String value; + @Field(value = "field.name.with.dots", nameType = Type.KEY) String value; Map mapValue; @@ -5034,7 +5087,8 @@ public boolean equals(Object o) { return false; } WithFieldNameContainingDots withFieldNameContainingDots = (WithFieldNameContainingDots) o; - return Objects.equals(id, withFieldNameContainingDots.id) && Objects.equals(value, withFieldNameContainingDots.value) + return Objects.equals(id, withFieldNameContainingDots.id) + && Objects.equals(value, withFieldNameContainingDots.value) && Objects.equals(mapValue, withFieldNameContainingDots.mapValue); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 1f0af2851e..6ec5630644 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -90,6 +90,8 @@ import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.NestedType; import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -135,8 +137,8 @@ class MappingMongoConverterUnitTests { @BeforeEach void beforeEach() { - MongoCustomConversions conversions = new MongoCustomConversions( - Arrays.asList(new ByteBufferToDoubleHolderConverter())); + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter() + .registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(BigDecimalRepresentation.DECIMAL128)); mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -396,6 +398,31 @@ void writesClassWithBigDecimal() { assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5")); assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); + } // MappingMongoConverterUnitTests + + @Test // DATACMNS-42, DATAMONGO-171, GH-4920 + void writesClassWithBigDecimalFails() { + + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter()); + + mappingContext = new MongoMappingContext(); + mappingContext.setApplicationContext(context); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + mappingContext.afterPropertiesSet(); + + mappingContext.getPersistentEntity(Address.class); + + converter = new MappingMongoConverter(resolver, mappingContext); + + BigDecimalContainer container = new BigDecimalContainer(); + container.value = BigDecimal.valueOf(2.5d); + container.map = Collections.singletonMap("foo", container.value); + + org.bson.Document document = new org.bson.Document(); + converter.write(container, document); + + assertThat(document.get("value")).isInstanceOf(BigDecimal.class); + assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(BigDecimal.class); } @Test // DATACMNS-42, DATAMONGO-171 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 8de30fd1fe..e950036087 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -55,6 +55,7 @@ import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.BigDecimalRepresentation; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.mapping.DBRef; @@ -97,7 +98,7 @@ public class QueryMapperUnitTests { @BeforeEach void beforeEach() { - MongoCustomConversions conversions = new MongoCustomConversions(); + MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter().bigDecimal(BigDecimalRepresentation.DECIMAL128)); this.context = new MongoMappingContext(); this.context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); @@ -152,6 +153,25 @@ void handlesBigIntegerIdAsDecimal128Correctly() { assertThat(result).containsEntry("_id", Decimal128.parse("1")); } + @Test // GH-5037 + void leavesBigIntegerAsIsIfNotConfigured() { + + MongoCustomConversions conversions = new MongoCustomConversions(); + context = new MongoMappingContext(); + context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + + converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); + + mapper = new QueryMapper(converter); + + org.bson.Document document = new org.bson.Document("id", new BigInteger("1")); + + org.bson.Document result = mapper.getMappedObject(document, context.getPersistentEntity(IdWrapper.class)); + assertThat(result).containsEntry("_id", new BigInteger("1")); + } + @Test void handlesObjectIdCapableBigIntegerIdsCorrectly() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java index 44b7ae3e45..97b30b1443 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoConverterConfigurer.java @@ -16,10 +16,12 @@ package org.springframework.data.mongodb.test.util; import java.util.Arrays; +import java.util.function.Consumer; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; /** * Utility to configure {@link MongoCustomConversions}. @@ -37,4 +39,8 @@ public void customConversions(CustomConversions customConversions) { public void customConverters(Converter... converters) { customConversions(new MongoCustomConversions(Arrays.asList(converters))); } + + public void customConverters(Consumer configurer) { + customConversions(MongoCustomConversions.create(configurer)); + } } diff --git a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc index 48e08cdd43..fd93c848dd 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -36,3 +36,22 @@ XML:: ---- ====== + +== BigInteger/BigDecimal Conversion Changes + +Spring Data no longer defaults BigInteger/BigDecimal conversion via its configuration support classes. +In order to persist those values the `BigDecimalRepresentation` hast to be set explicitly. + +[source,java] +---- +@Configuration +static class Config extends AbstractMongoClientConfiguration { + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter configAdapter) { + configAdapter.bigDecimal(BigDecimalRepresentation.DECIMAL128); + } + + // ... +} +---- From 12129c4c774414eda318fe943fe55fd7aa20850b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 8 Sep 2025 11:21:14 +0200 Subject: [PATCH 5/8] Make sure big number representation allows for backwards compatibility. Extend configuration options in a way that allows to register different conversion options for numeric values so users can recreate the 4.x default behaviour using String as default while honoring field specific configuration via the targetType attribute of the Field Annotation. --- .../core/convert/MongoCustomConversions.java | 20 +++-- .../MappingMongoConverterUnitTests.java | 89 ++++++++++++++++--- .../migration-guide-4.x-to-5.x.adoc | 5 +- .../mongodb/mapping/custom-conversions.adoc | 11 +-- 4 files changed, 99 insertions(+), 26 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index b68c8189e4..28bc092888 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -155,7 +155,7 @@ public static class MongoConverterConfigurationAdapter { LocalDateTime.class); private boolean useNativeDriverJavaTimeCodecs = false; - private @Nullable BigDecimalRepresentation bigDecimals; + private BigDecimalRepresentation @Nullable [] bigDecimals; private final List customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); @@ -312,14 +312,14 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { * Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in * MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}. * - * @param representation the representation to use. + * @param representations ordered list of representations to use (first one is default) * @return this. * @since 4.5 */ - public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) { + public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) { - Assert.notNull(representation, "BigDecimalDataType must not be null"); - this.bigDecimals = representation; + Assert.notEmpty(representations, "BigDecimalDataType must not be null"); + this.bigDecimals = representations; return this; } @@ -375,13 +375,15 @@ ConverterConfiguration createConverterConfiguration() { List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); if (bigDecimals != null) { - switch (bigDecimals) { - case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); - case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + for (BigDecimalRepresentation representation : bigDecimals) { + switch (representation) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + } } } else if (LOGGER.isInfoEnabled()) { LOGGER.info( - "No BigDecimal/BigInteger representation set. Choose [STRING] or [DECIMAL128] to store values in desired format."); + "No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format."); } if (useNativeDriverJavaTimeCodecs) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 6ec5630644..2eea1ecd78 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -15,10 +15,22 @@ */ package org.springframework.data.mongodb.core.convert; -import static java.time.ZoneId.*; -import static org.mockito.Mockito.*; -import static org.springframework.data.mongodb.core.DocumentTestUtils.*; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static java.time.ZoneId.systemDefault; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint; +import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatThrownBy; +import static org.springframework.data.mongodb.test.util.Assertions.fail; import java.math.BigDecimal; import java.math.BigInteger; @@ -53,7 +65,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +73,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; @@ -2176,6 +2188,61 @@ void mapsBigDecimalToDecimal128WhenAnnotatedWithFieldTargetType() { assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigIntegerToDecimal128WhenAnnotatedWithFieldTargetTypeWhenDefaultConversionIsSetToString() { + + converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigDecimal = BigDecimal.valueOf(3.14159D); + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + @Test // GH-5037 + @SuppressWarnings("deprecation") + void mapsBigIntegerToStringWhenNotAnnotatedWithFieldTargetTypeAndDefaultConversionIsSetToString() { + + converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + + BigDecimalContainer source = new BigDecimalContainer(); + source.value = BigDecimal.valueOf(3.14159D); + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + assertThat(target.get("value")).isInstanceOf(String.class); + } + + @Test // GH-5037 + void mapsBigIntegerToStringWhenAnnotatedWithFieldTargetTypeEvenWhenDefaultConverterIsSetToDecimal128() { + + converter = createConverter(BigDecimalRepresentation.DECIMAL128); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigIntegerAsString = BigInteger.TWO; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString()); + } + + @Test // GH-5037 + void explicitBigNumberConversionErrorsIfConverterNotRegistered() { + + converter = createConverter(BigDecimalRepresentation.STRING); + + WithExplicitTargetTypes source = new WithExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + + org.bson.Document target = new org.bson.Document(); + + assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> converter.write(source, target)); + } + @Test // DATAMONGO-2328 void mapsDateToLongWhenAnnotatedWithFieldTargetType() { @@ -3198,7 +3265,6 @@ void beanConverter() { return nativeValue.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); @@ -3435,7 +3501,7 @@ void usesStringNumericFormat() { } private MappingMongoConverter createConverter( - MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { + MongoCustomConversions.BigDecimalRepresentation... bigDecimalRepresentation) { MongoCustomConversions conversions = MongoCustomConversions.create( it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); @@ -4108,7 +4174,11 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; - @Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; + @Field(targetType = FieldType.DECIMAL128) // + BigInteger bigInteger; + + @Field(targetType = FieldType.STRING) // + BigInteger bigIntegerAsString; @Field(targetType = FieldType.INT64) // Date dateAsLong; @@ -4238,7 +4308,6 @@ public SubTypeOfGenericType convert(org.bson.Document source) { @WritingConverter static class TypeImplementingMapToDocumentConverter implements Converter { - @Override public org.bson.@Nullable Document convert(TypeImplementingMap source) { return new org.bson.Document("1st", source.val1).append("2nd", source.val2); @@ -4440,7 +4509,6 @@ enum Converter2 implements MongoValueConverter { return value.getString("bar"); } - @Override public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("bar", value); @@ -4454,7 +4522,6 @@ static class Converter1 implements MongoValueConverter String _id_ values that represent a valid `ObjectId` are converted automatically. See xref:mongodb/template-crud-operations.adoc#mongo-template.id-handling[How the `_id` Field is Handled in the Mapping Layer] for details. <2> The desired target type is explicitly defined as `String`. -Otherwise, the -`BigDecimal` value would have been turned into a `Decimal128`. +Otherwise. <3> `Date` values are handled by the MongoDB driver itself are stored as `ISODate`. ==== @@ -113,8 +112,10 @@ To persist `BigDecimal` and `BigInteger` values, Spring Data MongoDB converted v This approach had several downsides due to lexical instead of numeric comparison for queries, updates, etc. With MongoDB Server 3.4, `org.bson.types.Decimal128` offers a native representation for `BigDecimal` and `BigInteger`. -As of Spring Data MongoDB 5.0. the default representation of those types moved to MongoDB native `org.bson.types.Decimal128`. -You can still use the to the previous `String` variant by configuring the big decimal representation in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING))`. +As of Spring Data MongoDB 5.0. there no longer is a default representation of those types and conversion needs to be configured explicitly. +You can register multiple formats, 1st being default, and still retain the previous behaviour by configuring the `BigDecimalRepresentation` in `MongoCustomConversions` through `MongoCustomConversions.create(config -> config.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128))`. +This allows you to make use of the explicit storage type format via `@Field(targetType = DECIMAL128)` while keeping default conversion set to String. +Choosing none of the provided representations is valid as long as those values are no persisted. [NOTE] ==== From 4cdc415fef79a6e4192afcd6cbfb20c94bcda934 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 8 Sep 2025 11:21:14 +0200 Subject: [PATCH 6/8] Make sure big number representation allows for backwards compatibility. Extend configuration options in a way that allows to register different conversion options for numeric values so users can recreate the 4.x default behaviour using String as default while honoring field specific configuration via the targetType attribute of the Field Annotation. --- .../MappingMongoConverterUnitTests.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 2eea1ecd78..e3982a6ac0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -39,7 +39,24 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; From 519bd894967773550c988134515a32ab3e3912ad Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 8 Sep 2025 16:48:10 +0200 Subject: [PATCH 7/8] Polishing. Register BigDecimal and BigInteger as simple types to allow explicitly typed writes and register converters in the ConversionService directly to avoid forcing a specific type. Revert BigDecimalRepresentation list change to use only one representation. In a sense, we're aligning with MongoDB's driver behavior that BigDecimal now maps by default to Decimal128, while BigInteger requires explicit configuration. --- .../core/convert/MappingMongoConverter.java | 1 + .../mongodb/core/convert/MongoConverters.java | 13 +- .../core/convert/MongoCustomConversions.java | 81 +++++-- .../core/mapping/MongoSimpleTypes.java | 3 +- .../MappingMongoConverterUnitTests.java | 198 +++++++++++++----- 5 files changed, 216 insertions(+), 80 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 24c3c2f590..fbd5d82cac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -1376,6 +1376,7 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist if (typeHint != null && Object.class != typeHint) { + // TODO this is weird and leads to double-conversion in some cases, e.g. BigDecimal -> Decimal128 -> BigDecimal if (conversionService.canConvert(value.getClass(), typeHint)) { value = doConvert(value, typeHint); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index 2a8c8042a2..5cfd6fe72a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -110,9 +110,9 @@ static Collection getDateToUtcConverters() { * @return * @since 5.0 */ - static Collection getBigNumberDecimal128Converters() { + static Collection> getBigNumberDecimal128Converters() { - List converters = new ArrayList<>(3); + List> converters = new ArrayList<>(3); converters.add(BigDecimalToDecimal128Converter.INSTANCE); converters.add(Decimal128ToBigDecimalConverter.INSTANCE); @@ -127,9 +127,9 @@ static Collection getBigNumberDecimal128Converters() { * @return * @since 5.0 */ - static Collection getBigNumberStringConverters() { + static Collection> getBigNumberStringConverters() { - List converters = new ArrayList<>(4); + List> converters = new ArrayList<>(2); converters.add(BigDecimalToStringConverter.INSTANCE); converters.add(BigIntegerToStringConverter.INSTANCE); @@ -228,6 +228,7 @@ public ObjectId convert(BigInteger source) { } } + @WritingConverter enum BigDecimalToStringConverter implements Converter { INSTANCE; @@ -239,6 +240,7 @@ public String convert(BigDecimal source) { /** * @since 2.2 */ + @WritingConverter enum BigDecimalToDecimal128Converter implements Converter { INSTANCE; @@ -250,6 +252,7 @@ public Decimal128 convert(BigDecimal source) { /** * @since 5.0 */ + @WritingConverter enum BigIntegerToDecimal128Converter implements Converter { INSTANCE; @@ -258,6 +261,7 @@ public Decimal128 convert(BigInteger source) { } } + @ReadingConverter enum StringToBigDecimalConverter implements Converter { INSTANCE; @@ -269,6 +273,7 @@ enum StringToBigDecimalConverter implements Converter { /** * @since 2.2 */ + @ReadingConverter enum Decimal128ToBigDecimalConverter implements Converter { INSTANCE; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index 28bc092888..6915ae5b5f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -30,13 +30,16 @@ import java.util.Locale; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.PropertyValueConversions; @@ -78,6 +81,12 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus STORE_CONVERTERS = Collections.unmodifiableList(converters); } + /** + * Converters to be registered with the {@code ConversionService} but hidden from CustomConversions to avoid + * converter-based type hinting. + */ + private final List> fallbackConversionServiceConverters = new ArrayList<>(); + /** * Creates an empty {@link MongoCustomConversions} object. */ @@ -101,7 +110,12 @@ public MongoCustomConversions(List converters) { * @since 2.3 */ protected MongoCustomConversions(MongoConverterConfigurationAdapter conversionConfiguration) { - super(conversionConfiguration.createConverterConfiguration()); + this(conversionConfiguration.createConverterConfiguration()); + } + + private MongoCustomConversions(MongoConverterConfiguration converterConfiguration) { + super(converterConfiguration); + this.fallbackConversionServiceConverters.addAll(converterConfiguration.fallbackConversionServiceConverters); } /** @@ -120,6 +134,12 @@ public static MongoCustomConversions create(Consumer customConverters = new ArrayList<>(); private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {}); @@ -312,14 +332,14 @@ public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { * Configures the representation to for {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in * MongoDB. Defaults to {@link BigDecimalRepresentation#DECIMAL128}. * - * @param representations ordered list of representations to use (first one is default) + * @param representation the representation to use. * @return this. * @since 4.5 */ - public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation... representations) { + public MongoConverterConfigurationAdapter bigDecimal(BigDecimalRepresentation representation) { - Assert.notEmpty(representations, "BigDecimalDataType must not be null"); - this.bigDecimals = representations; + Assert.notNull(representation, "BigDecimalDataType must not be null"); + this.bigDecimals = representation; return this; } @@ -365,7 +385,7 @@ PropertyValueConversions valueConversions() { return this.propertyValueConversions; } - ConverterConfiguration createConverterConfiguration() { + MongoConverterConfiguration createConverterConfiguration() { if (hasDefaultPropertyValueConversions() && propertyValueConversions instanceof SimplePropertyValueConversions svc) { @@ -373,19 +393,24 @@ ConverterConfiguration createConverterConfiguration() { } List storeConverters = new ArrayList<>(STORE_CONVERTERS.size() + 10); - - if (bigDecimals != null) { - for (BigDecimalRepresentation representation : bigDecimals) { - switch (representation) { - case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); - case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); - } + List> fallbackConversionServiceConverters = new ArrayList<>(5); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberStringConverters()); + fallbackConversionServiceConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); + + if (bigDecimals == null) { + if (LOGGER.isInfoEnabled()) { + LOGGER.info( + "No BigDecimal/BigInteger representation set. Choose 'BigDecimalRepresentation.DECIMAL128' or 'BigDecimalRepresentation.String' to store values in desired format."); + } + } else { + switch (bigDecimals) { + case STRING -> storeConverters.addAll(MongoConverters.getBigNumberStringConverters()); + case DECIMAL128 -> storeConverters.addAll(MongoConverters.getBigNumberDecimal128Converters()); } - } else if (LOGGER.isInfoEnabled()) { - LOGGER.info( - "No BigDecimal/BigInteger representation set. Choose [DECIMAL128] and/or [String] to store values in desired format."); } + fallbackConversionServiceConverters.removeAll(storeConverters); + if (useNativeDriverJavaTimeCodecs) { /* @@ -397,7 +422,8 @@ ConverterConfiguration createConverterConfiguration() { StoreConversions storeConversions = StoreConversions .of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES, MongoSimpleTypes.HOLDER), storeConverters); - return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> { + return new MongoConverterConfiguration(storeConversions, fallbackConversionServiceConverters, + this.customConverters, convertiblePair -> { // Avoid default registrations @@ -408,8 +434,10 @@ ConverterConfiguration createConverterConfiguration() { } storeConverters.addAll(STORE_CONVERTERS); - return new ConverterConfiguration(StoreConversions.of(MongoSimpleTypes.createSimpleTypeHolder(), storeConverters), - this.customConverters, convertiblePair -> true, this.propertyValueConversions); + return new MongoConverterConfiguration( + StoreConversions.of(MongoSimpleTypes.createSimpleTypeHolder(), storeConverters), + fallbackConversionServiceConverters, this.customConverters, convertiblePair -> true, + this.propertyValueConversions); } private boolean hasDefaultPropertyValueConversions() { @@ -418,6 +446,19 @@ private boolean hasDefaultPropertyValueConversions() { } + static class MongoConverterConfiguration extends ConverterConfiguration { + + private final List> fallbackConversionServiceConverters; + + public MongoConverterConfiguration(StoreConversions storeConversions, + List> fallbackConversionServiceConverters, List userConverters, + Predicate converterRegistrationFilter, + @Nullable PropertyValueConversions propertyValueConversions) { + super(storeConversions, userConverters, converterRegistrationFilter, propertyValueConversions); + this.fallbackConversionServiceConverters = fallbackConversionServiceConverters; + } + } + /** * Strategy to represent {@link java.math.BigDecimal} and {@link java.math.BigInteger} values in MongoDB. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index aeb79fde40..2c1b6af660 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.mapping; +import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; import java.util.Set; @@ -60,7 +61,7 @@ public abstract class MongoSimpleTypes { BsonDocument.class, BsonDouble.class, BsonInt32.class, BsonInt64.class, BsonJavaScript.class, BsonJavaScriptWithScope.class, BsonObjectId.class, BsonRegularExpression.class, BsonString.class, BsonTimestamp.class, Geometry.class, GeometryCollection.class, LineString.class, MultiLineString.class, - MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class); + MultiPoint.class, MultiPolygon.class, Point.class, Polygon.class, BigInteger.class, BigDecimal.class); public static final SimpleTypeHolder HOLDER = createSimpleTypeHolder(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index e3982a6ac0..d3cc037fe8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -15,22 +15,10 @@ */ package org.springframework.data.mongodb.core.convert; -import static java.time.ZoneId.systemDefault; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.data.mongodb.core.DocumentTestUtils.assertTypeHint; -import static org.springframework.data.mongodb.core.DocumentTestUtils.getAsDocument; -import static org.springframework.data.mongodb.test.util.Assertions.assertThat; -import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; -import static org.springframework.data.mongodb.test.util.Assertions.assertThatThrownBy; -import static org.springframework.data.mongodb.test.util.Assertions.fail; +import static java.time.ZoneId.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.core.DocumentTestUtils.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import java.math.BigDecimal; import java.math.BigInteger; @@ -39,24 +27,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.UUID; +import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -82,6 +53,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.factory.annotation.Autowired; @@ -90,7 +62,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; @@ -2207,57 +2178,153 @@ void mapsBigDecimalToDecimal128WhenAnnotatedWithFieldTargetType() { @Test // GH-5037 @SuppressWarnings("deprecation") - void mapsBigIntegerToDecimal128WhenAnnotatedWithFieldTargetTypeWhenDefaultConversionIsSetToString() { + void mapsBigNumbersToString() { - converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + converter = createConverter(BigDecimalRepresentation.STRING); - WithExplicitTargetTypes source = new WithExplicitTargetTypes(); - source.bigDecimal = BigDecimal.valueOf(3.14159D); + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); org.bson.Document target = new org.bson.Document(); converter.write(source, target); - assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger.toString()); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal.toString()); } @Test // GH-5037 - @SuppressWarnings("deprecation") - void mapsBigIntegerToStringWhenNotAnnotatedWithFieldTargetTypeAndDefaultConversionIsSetToString() { + void mapsBigNumbersToDecimal128() { - converter = createConverter(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128); + converter = createConverter(BigDecimalRepresentation.DECIMAL128); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("3.14159"); - BigDecimalContainer source = new BigDecimalContainer(); - source.value = BigDecimal.valueOf(3.14159D); org.bson.Document target = new org.bson.Document(); converter.write(source, target); - assertThat(target.get("value")).isInstanceOf(String.class); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } - @Test // GH-5037 - void mapsBigIntegerToStringWhenAnnotatedWithFieldTargetTypeEvenWhenDefaultConverterIsSetToDecimal128() { + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitBigIntegerToStringConversion(BigDecimalRepresentation representation) { - converter = createConverter(BigDecimalRepresentation.DECIMAL128); + converter = createConverter(representation); WithExplicitTargetTypes source = new WithExplicitTargetTypes(); source.bigIntegerAsString = BigInteger.TWO; + source.bigDecimalAsString = new BigDecimal("123.456"); org.bson.Document target = new org.bson.Document(); converter.write(source, target); assertThat(target.get("bigIntegerAsString")).isEqualTo(source.bigIntegerAsString.toString()); + assertThat(target.get("bigDecimalAsString")).isEqualTo(source.bigDecimalAsString.toString()); } - @Test // GH-5037 - void explicitBigNumberConversionErrorsIfConverterNotRegistered() { + @ParameterizedTest // GH-5037 + @MethodSource("representations") + void shouldApplyExplicitDecimal128Conversion(BigDecimalRepresentation representation) { - converter = createConverter(BigDecimalRepresentation.STRING); + converter = createConverter(representation); WithExplicitTargetTypes source = new WithExplicitTargetTypes(); source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(new Decimal128(source.bigInteger.longValue())); + assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); + } + + static Stream representations() { + return Stream.of(Arguments.argumentSet("None (default)", new Object[] { null }), // + Arguments.argumentSet("STRING", BigDecimalRepresentation.STRING), // + Arguments.argumentSet("DECIMAL128", BigDecimalRepresentation.DECIMAL128)); + } + + @Test // GH-5037 + void shouldWriteBigNumbersAsIsWithoutConfiguration() { + + converter = createConverter(); + + WithoutExplicitTargetTypes source = new WithoutExplicitTargetTypes(); + source.bigInteger = BigInteger.TWO; + source.bigDecimal = new BigDecimal("123.456"); + + org.bson.Document target = new org.bson.Document(); + + converter.write(source, target); + + assertThat(target.get("bigInteger")).isEqualTo(source.bigInteger); + assertThat(target.get("bigDecimal")).isEqualTo(source.bigDecimal); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromDecimal128() { + + converter = createConverter(); org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); - assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> converter.write(source, target)); + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadTypedBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigIntegerAsString", "2"); + target.put("bigDecimalAsString", "123.456"); + + WithExplicitTargetTypes result = converter.read(WithExplicitTargetTypes.class, target); + + assertThat(result.bigIntegerAsString).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimalAsString).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromDecimal128() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", new Decimal128(2)); + target.put("bigDecimal", new Decimal128(new BigDecimal("123.456"))); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); + } + + @Test // GH-5037 + void shouldReadBigNumbersFromString() { + + converter = createConverter(); + + org.bson.Document target = new org.bson.Document(); + target.put("bigInteger", "2"); + target.put("bigDecimal", "123.456"); + + WithoutExplicitTargetTypes result = converter.read(WithoutExplicitTargetTypes.class, target); + + assertThat(result.bigInteger).isEqualTo(BigInteger.TWO); + assertThat(result.bigDecimal).isEqualTo(new BigDecimal("123.456")); } @Test // DATAMONGO-2328 @@ -3517,11 +3584,21 @@ void usesStringNumericFormat() { assertThat(document).containsEntry("map.foo", "2.5"); } + private MappingMongoConverter createConverter() { + return createConverter(null); + } + private MappingMongoConverter createConverter( - MongoCustomConversions.BigDecimalRepresentation... bigDecimalRepresentation) { + MongoCustomConversions.BigDecimalRepresentation bigDecimalRepresentation) { MongoCustomConversions conversions = MongoCustomConversions.create( - it -> it.registerConverter(new ByteBufferToDoubleHolderConverter()).bigDecimal(bigDecimalRepresentation)); + it -> { + it.registerConverter(new ByteBufferToDoubleHolderConverter()); + + if (bigDecimalRepresentation != null) { + it.bigDecimal(bigDecimalRepresentation); + } + }); MongoMappingContext mappingContext = new MongoMappingContext(); mappingContext.setApplicationContext(context); @@ -4197,6 +4274,9 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.STRING) // BigInteger bigIntegerAsString; + @Field(targetType = FieldType.STRING) // + BigDecimal bigDecimalAsString; + @Field(targetType = FieldType.INT64) // Date dateAsLong; @@ -4210,6 +4290,14 @@ static class WithExplicitTargetTypes { Date dateAsObjectId; } + static class WithoutExplicitTargetTypes { + + BigDecimal bigDecimal; + + BigInteger bigInteger; + + } + static class WrapperAroundWithUnwrapped { String someValue; From 74847489a7128bd9850e785e0f4d1eef295821c8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 9 Sep 2025 09:54:22 +0200 Subject: [PATCH 8/8] Update documentation --- .../convert/MappingMongoConverterUnitTests.java | 15 ++++----------- .../migration-guide-4.x-to-5.x.adoc | 3 +-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index d3cc037fe8..6523b196bf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -398,21 +398,12 @@ void writesClassWithBigDecimal() { assertThat(document.get("value")).isEqualTo(Decimal128.parse("2.5")); assertThat(((org.bson.Document) document.get("map")).get("foo")).isInstanceOf(Decimal128.class); - } // MappingMongoConverterUnitTests + } @Test // DATACMNS-42, DATAMONGO-171, GH-4920 void writesClassWithBigDecimalFails() { - MongoCustomConversions conversions = new MongoCustomConversions(new MongoConverterConfigurationAdapter()); - - mappingContext = new MongoMappingContext(); - mappingContext.setApplicationContext(context); - mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); - mappingContext.afterPropertiesSet(); - - mappingContext.getPersistentEntity(Address.class); - - converter = new MappingMongoConverter(resolver, mappingContext); + converter = createConverter(); BigDecimalContainer container = new BigDecimalContainer(); container.value = BigDecimal.valueOf(2.5d); @@ -2244,7 +2235,9 @@ void shouldApplyExplicitDecimal128Conversion(BigDecimalRepresentation representa assertThat(target.get("bigDecimal")).isEqualTo(new Decimal128(source.bigDecimal)); } + @SuppressWarnings("deprecation") static Stream representations() { + return Stream.of(Arguments.argumentSet("None (default)", new Object[] { null }), // Arguments.argumentSet("STRING", BigDecimalRepresentation.STRING), // Arguments.argumentSet("DECIMAL128", BigDecimalRepresentation.DECIMAL128)); diff --git a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc index 898d702368..4351cc70e7 100644 --- a/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc +++ b/src/main/antora/modules/ROOT/pages/migration-guide/migration-guide-4.x-to-5.x.adoc @@ -56,5 +56,4 @@ static class Config extends AbstractMongoClientConfiguration { } ---- -Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default. -Those using`@Field(targetType = FieldType.DECIMAL128)` need to define a combination of representations `configAdapter.bigDecimal(BigDecimalRepresentation.STRING, BigDecimalRepresentation.DECIMAL128)` to set defaulting to String while having the `DECIMAL128` converter being registered for usage with explicit target type configuration. +Users upgrading from prior versions may choose `BigDecimalRepresentation.STRING` as default to retain previous behaviour.