From b1effbbf08a563bc8b2016067790b73876db3a77 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 10 Feb 2026 15:14:02 +0100 Subject: [PATCH 1/5] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7babcc07a4..205d58cb5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 4.1.0-SNAPSHOT + 4.1.0-GH-3306-SNAPSHOT Spring Data Redis Spring Data module for Redis From 0febebd6fe71c3bbdbd292ee8a485e66543f4283 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 10 Feb 2026 15:40:04 +0100 Subject: [PATCH 2/5] Consider `DefaultTyping` configuration in Jackson Serializers. We're exploring whether it makes sense to accept a `DefaultTyping` config option for Jackson serializer configuration. --- .../GenericJackson2JsonRedisSerializer.java | 56 +++++++++++++--- .../GenericJacksonJsonRedisSerializer.java | 64 +++++++++++++++---- ...cJackson2JsonRedisSerializerUnitTests.java | 35 ++++++++++ ...icJacksonJsonRedisSerializerUnitTests.java | 44 +++++++++++-- 4 files changed, 170 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index 8865b6359e..bc768c0840 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -135,7 +135,8 @@ public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, registerNullValueSerializer(this.mapper, typeHintPropertyName); - this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(getObjectMapper(), typeHintPropertyName)); + this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder( + GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING, getObjectMapper(), typeHintPropertyName)); } /** @@ -216,10 +217,12 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM }); } - private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(ObjectMapper objectMapper, + private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTyping defaultTyping, + ObjectMapper objectMapper, @Nullable String typeHintPropertyName) { - StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(objectMapper).init(JsonTypeInfo.Id.CLASS, null) + StdTypeResolverBuilder typer = TypeResolverBuilder.forTyping(defaultTyping, objectMapper) + .init(JsonTypeInfo.Id.CLASS, null) .inclusion(As.PROPERTY); if (StringUtils.hasText(typeHintPropertyName)) { @@ -464,6 +467,8 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri */ public static class GenericJackson2JsonRedisSerializerBuilder { + private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.EVERYTHING; + private @Nullable String typeHintPropertyName; private Jackson2ObjectReader reader = Jackson2ObjectReader.create(); @@ -472,7 +477,9 @@ public static class GenericJackson2JsonRedisSerializerBuilder { private @Nullable ObjectMapper objectMapper; - private @Nullable Boolean defaultTyping; + private @Nullable Boolean defaultTypingEnabled; + + private @Nullable DefaultTyping defaultTyping; private boolean registerNullValueSerializer = true; @@ -490,6 +497,22 @@ private GenericJackson2JsonRedisSerializerBuilder() {} * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}. */ public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) { + this.defaultTypingEnabled = defaultTyping; + this.defaultTyping = defaultTyping ? DEFAULT_TYPING : null; + return this; + } + + /** + * Enable default typing by setting {@link DefaultTyping}. Enabling default typing will override + * {@link ObjectMapper#setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder)} for a given + * {@link ObjectMapper}. Default typing is enabled by default if no {@link ObjectMapper} is provided. + * + * @param defaultTyping the default typing mode. + * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}. + * @since 4.0.3 + */ + public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTyping defaultTyping) { + this.defaultTypingEnabled = true; this.defaultTyping = defaultTyping; return this; } @@ -599,9 +622,11 @@ public GenericJackson2JsonRedisSerializer build() { : new NullValueSerializer(this.typeHintPropertyName))); } - if ((!providedObjectMapper && (defaultTyping == null || defaultTyping)) - || (defaultTyping != null && defaultTyping)) { - objectMapper.setDefaultTyping(createDefaultTypeResolverBuilder(objectMapper, typeHintPropertyName)); + // enable default typing by default unless providing ObjectMapper or defaultTypingEnabled is explicitly set. + if ((!providedObjectMapper && (defaultTypingEnabled == null || defaultTypingEnabled)) + || (defaultTypingEnabled != null && defaultTypingEnabled)) { + objectMapper + .setDefaultTyping(createDefaultTypeResolverBuilder(defaultTyping, objectMapper, typeHintPropertyName)); } return new GenericJackson2JsonRedisSerializer(objectMapper, this.reader, this.writer, this.typeHintPropertyName); @@ -619,12 +644,17 @@ public GenericJackson2JsonRedisSerializer build() { */ private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { - static TypeResolverBuilder forEverything(ObjectMapper mapper) { - return new TypeResolverBuilder(DefaultTyping.EVERYTHING, mapper.getPolymorphicTypeValidator()); + private final DefaultTyping typing; + + static TypeResolverBuilder forTyping(@Nullable DefaultTyping defaultTyping, ObjectMapper mapper) { + return new TypeResolverBuilder( + defaultTyping == null ? GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING : defaultTyping, + mapper.getPolymorphicTypeValidator()); } public TypeResolverBuilder(DefaultTyping typing, PolymorphicTypeValidator polymorphicTypeValidator) { super(typing, polymorphicTypeValidator); + this.typing = typing; } @Override @@ -646,6 +676,10 @@ public boolean useForType(JavaType javaType) { javaType = resolveArrayOrWrapper(javaType); + if (javaType.isEnumType() && typing != DefaultTyping.EVERYTHING) { + return super.useForType(javaType); + } + if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) { return false; } @@ -655,6 +689,10 @@ public boolean useForType(JavaType javaType) { return false; } + if (typing != GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING) { + return super.useForType(javaType); + } + // [databind#88] Should not apply to JSON tree models: return !TreeNode.class.isAssignableFrom(javaType.getRawClass()); } diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java index e486a3f7a5..69b3849cc8 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java @@ -263,10 +263,12 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM */ public static class GenericJacksonJsonRedisSerializerBuilder>> { + private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.NON_FINAL; + private final Supplier builderFactory; private boolean cacheNullValueSupportEnabled = false; - private boolean defaultTyping = false; + private @Nullable DefaultTyping defaultTyping = null; private @Nullable String typePropertyName; private PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder() .allowIfBaseType(Object.class).allowIfSubType((ctx, clazz) -> true).build(); @@ -323,7 +325,28 @@ public GenericJacksonJsonRedisSerializerBuilder enableSpringCacheNullValueSup @Contract("-> this") public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() { - this.defaultTyping = true; + withDefaultTyping(); + return this; + } + + /** + * Enables + * {@link JsonMapper.Builder#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String) + * default typing} without any type validation constraints. + *

+ * WARNING: without restrictions of the {@link PolymorphicTypeValidator} deserialization is + * vulnerable to arbitrary code execution when reading from untrusted sources. + * + * @param defaultTyping the default typing mode to use. + * @return {@code this} builder. + * @since 4.0.3 + * @see https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data + */ + @Contract("_ -> this") + public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTyping defaultTyping) { + + this.defaultTyping = defaultTyping; return this; } @@ -338,8 +361,8 @@ public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() { public GenericJacksonJsonRedisSerializerBuilder enableDefaultTyping(PolymorphicTypeValidator typeValidator) { typeValidator(typeValidator); + withDefaultTyping(); - this.defaultTyping = true; return this; } @@ -372,6 +395,15 @@ public GenericJacksonJsonRedisSerializerBuilder typePropertyName(String typeP return this; } + /** + * Enable default typing using {@link DefaultTyping#NON_FINAL} if not already configured. + */ + private void withDefaultTyping() { + if (this.defaultTyping == null) { + defaultTyping(DEFAULT_TYPING); + } + } + /** * Configures the {@link JacksonObjectWriter}. * @@ -440,10 +472,10 @@ public GenericJacksonJsonRedisSerializer build() { })); } - if (defaultTyping) { + if (defaultTyping != null) { GenericJacksonJsonRedisSerializer.TypeResolverBuilder resolver = new GenericJacksonJsonRedisSerializer.TypeResolverBuilder( - typeValidator, DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, typePropertyName); + typeValidator, defaultTyping, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, typePropertyName); mapperBuilder.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).setDefaultTyping(resolver); @@ -596,17 +628,13 @@ public void setupModule(SetupContext context) { private static class TypeResolverBuilder extends DefaultTypeResolverBuilder { - public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, JsonTypeInfo.As includeAs) { - super(subtypeValidator, t, includeAs); - } - - public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, String propertyName) { - super(subtypeValidator, t, propertyName); - } + private final DefaultTyping defaultTyping; - public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, JsonTypeInfo.As includeAs, + public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping defaultTyping, + JsonTypeInfo.As includeAs, JsonTypeInfo.Id idType, @Nullable String propertyName) { - super(subtypeValidator, t, includeAs, idType, propertyName); + super(subtypeValidator, defaultTyping, includeAs, idType, propertyName); + this.defaultTyping = defaultTyping; } @Override @@ -628,6 +656,10 @@ public boolean useForType(JavaType javaType) { javaType = resolveArrayOrWrapper(javaType); + if (javaType.isEnumType() && defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) { + return super.useForType(javaType); + } + if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) { return false; } @@ -637,6 +669,10 @@ public boolean useForType(JavaType javaType) { return false; } + if (defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) { + return super.useForType(javaType); + } + // [databind#88] Should not apply to JSON tree models: return !TreeNode.class.isAssignableFrom(javaType.getRawClass()); } diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index c959637650..ac7bdb4152 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -64,6 +64,7 @@ * @author Mark Paluch * @author John Blum */ +@SuppressWarnings("removal") class GenericJackson2JsonRedisSerializerUnitTests { private static final SimpleObject SIMPLE_OBJECT = new SimpleObject(1L); @@ -402,6 +403,40 @@ void deserializesEnumFromBytes() { .isEqualTo(EnumType.TWO); } + @Test // GH-3306 + void serializesNonFinalIntoBytesWithTypeHint() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + + assertThat(new String(serializer.serialize(EnumType.ONE))) + .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); + } + + @Test // GH-3306 + void deserializesEnumFromBytesWithTypeHint() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + + assertThat(serializer.deserialize( + "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.TWO); + } + + @Test // GH-3306 + void serializesRecordIntoBytesWithout() { + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + + record Foo(String hello) { + + } + + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); + } + @Test // GH-2396 void serializesJavaTimeIntoBytes() { diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java index a3cec6c3d4..1f068a0f52 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java @@ -344,21 +344,53 @@ void deserializesUUIDFromBytes() { @Test // GH-2396 void serializesEnumIntoBytes() { - - GenericJacksonJsonRedisSerializer serializer = this.serializer; - - assertThat(serializer.serialize(EnumType.ONE)).isEqualTo(("\"ONE\"").getBytes(StandardCharsets.UTF_8)); + assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); } @Test // GH-2396 void deserializesEnumFromBytes() { - GenericJacksonJsonRedisSerializer serializer = this.serializer; - assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) .isEqualTo(EnumType.TWO); } + @Test // GH-3306 + void serializesEnumIntoBytesWithTypeHint() { + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { + it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); + }); + + assertThat(new String(serializer.serialize(EnumType.ONE))) + .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); + } + + @Test // GH-3306 + void deserializesEnumFromBytesWithTypeHint() { + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { + it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); + }); + + assertThat(serializer.deserialize( + "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.TWO); + } + + @Test // GH-3306 + void serializesRecordIntoBytesWithoutHint() { + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { + it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); + }); + + record Foo(String hello) { + + } + + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); + } + @Test // GH-2396 void serializesJavaTimeIntoBytes() { From 2a128aa3a5090ca7ddc86c4cb7f3dcc23aca6dbd Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 31 Mar 2026 22:16:18 -0500 Subject: [PATCH 3/5] Add default type mapping config for Jackson serializer. --- .../serializer/DefaultTypingPredicate.java | 80 ++++++++++ .../GenericJackson2JsonRedisSerializer.java | 64 +++----- .../GenericJacksonJsonRedisSerializer.java | 89 +++++------- .../serializer/StdDefaultTypingPredicate.java | 83 +++++++++++ ...cJackson2JsonRedisSerializerUnitTests.java | 123 ++++++++++++++-- ...icJacksonJsonRedisSerializerUnitTests.java | 137 +++++++++++++++--- 6 files changed, 450 insertions(+), 126 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java b/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java new file mode 100644 index 0000000000..266cc9ef7c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java @@ -0,0 +1,80 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import java.lang.reflect.Modifier; +import java.util.function.Predicate; +import org.springframework.util.ClassUtils; + +/** + * @author Chris Bono + * @since 4.1 + */ +@FunctionalInterface +public interface DefaultTypingPredicate { + + enum Action { + YES, + NO, + DONT_CARE; + } + + /** + * Determine the action to take for a particular type. + * @param clazz the type to check. + * @return the action to take. + */ + Action test(Class clazz); + + /** + * Obtain a builder with no defaults configured. + * @return a builder with no defaults configured. + */ + static DefaultTypingPredicate.Builder empty() { + return new StdDefaultTypingPredicate.DefaultBuilder(); + } + + /** + * Obtain a builder with defaults configured. + * @return a builder with defaults configured. + */ + static DefaultTypingPredicate.Builder defaults() { + + DefaultTypingPredicate.Builder builder = new StdDefaultTypingPredicate.DefaultBuilder(); + builder.include((clazz) -> clazz == Object.class); + builder.exclude((clazz) -> Modifier.isFinal(clazz.getModifiers()) && clazz.getPackageName().startsWith("java")); + builder.exclude(ClassUtils::isPrimitiveOrWrapper); + builder.include(Class::isEnum); + builder.include(Class::isRecord); + + return builder; + } + + /** + * + */ + interface Builder { + + Builder include(Predicate> typeMatcher); + + Builder exclude(Predicate> typeMatcher); + + Builder dontCare(Predicate> typeMatcher); + + DefaultTypingPredicate build(); + } + +} diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index bc768c0840..ff535a8054 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -24,10 +24,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.cache.support.NullValue; -import org.springframework.core.KotlinDetector; import org.springframework.data.util.Lazy; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -68,6 +66,7 @@ * @author Mao Shuai * @author John Blum * @author Anne Lee + * @author Chris Bono * @see Jackson2ObjectReader * @see Jackson2ObjectWriter * @see com.fasterxml.jackson.databind.ObjectMapper @@ -135,8 +134,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, registerNullValueSerializer(this.mapper, typeHintPropertyName); - this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder( - GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING, getObjectMapper(), typeHintPropertyName)); + this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(null, getObjectMapper(), typeHintPropertyName)); } /** @@ -217,7 +215,7 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM }); } - private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTyping defaultTyping, + private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTypingPredicate defaultTyping, ObjectMapper objectMapper, @Nullable String typeHintPropertyName) { @@ -467,8 +465,6 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri */ public static class GenericJackson2JsonRedisSerializerBuilder { - private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.EVERYTHING; - private @Nullable String typeHintPropertyName; private Jackson2ObjectReader reader = Jackson2ObjectReader.create(); @@ -479,7 +475,7 @@ public static class GenericJackson2JsonRedisSerializerBuilder { private @Nullable Boolean defaultTypingEnabled; - private @Nullable DefaultTyping defaultTyping; + private @Nullable DefaultTypingPredicate defaultTyping; private boolean registerNullValueSerializer = true; @@ -498,7 +494,7 @@ private GenericJackson2JsonRedisSerializerBuilder() {} */ public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) { this.defaultTypingEnabled = defaultTyping; - this.defaultTyping = defaultTyping ? DEFAULT_TYPING : null; + this.defaultTyping = null; return this; } @@ -507,11 +503,11 @@ public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTy * {@link ObjectMapper#setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder)} for a given * {@link ObjectMapper}. Default typing is enabled by default if no {@link ObjectMapper} is provided. * - * @param defaultTyping the default typing mode. + * @param defaultTyping the predicate that matches whether the type should have type info hints added. * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}. - * @since 4.0.3 + * @since 4.0.4 */ - public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTyping defaultTyping) { + public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTypingPredicate defaultTyping) { this.defaultTypingEnabled = true; this.defaultTyping = defaultTyping; return this; @@ -644,17 +640,17 @@ public GenericJackson2JsonRedisSerializer build() { */ private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { - private final DefaultTyping typing; + private final @Nullable DefaultTypingPredicate defaultTyping; - static TypeResolverBuilder forTyping(@Nullable DefaultTyping defaultTyping, ObjectMapper mapper) { + static TypeResolverBuilder forTyping(@Nullable DefaultTypingPredicate defaultTyping, ObjectMapper mapper) { return new TypeResolverBuilder( - defaultTyping == null ? GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING : defaultTyping, + defaultTyping, mapper.getPolymorphicTypeValidator()); } - public TypeResolverBuilder(DefaultTyping typing, PolymorphicTypeValidator polymorphicTypeValidator) { - super(typing, polymorphicTypeValidator); - this.typing = typing; + public TypeResolverBuilder(@Nullable DefaultTypingPredicate defaultTyping, PolymorphicTypeValidator polymorphicTypeValidator) { + super(DefaultTyping.EVERYTHING, polymorphicTypeValidator); + this.defaultTyping = defaultTyping; } @Override @@ -670,31 +666,19 @@ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class defaultI @Override public boolean useForType(JavaType javaType) { - if (javaType.isJavaLangObject()) { - return true; - } - - javaType = resolveArrayOrWrapper(javaType); - - if (javaType.isEnumType() && typing != DefaultTyping.EVERYTHING) { - return super.useForType(javaType); - } + JavaType resolvedType = resolveArrayOrWrapper(javaType); + Class rawClass = resolvedType.getRawClass(); - if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) { - return false; - } + DefaultTypingPredicate typingPredicate = defaultTyping != null ? defaultTyping : + DefaultTypingPredicate.defaults().build(); - if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass()) - && javaType.getRawClass().getPackageName().startsWith("java")) { - return false; - } - - if (typing != GenericJackson2JsonRedisSerializerBuilder.DEFAULT_TYPING) { - return super.useForType(javaType); - } + DefaultTypingPredicate.Action action = typingPredicate.test(rawClass); - // [databind#88] Should not apply to JSON tree models: - return !TreeNode.class.isAssignableFrom(javaType.getRawClass()); + return switch (action) { + case YES -> true; + case NO -> false; + case DONT_CARE -> !TreeNode.class.isAssignableFrom(rawClass); + }; } private JavaType resolveArrayOrWrapper(JavaType type) { diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java index 69b3849cc8..7ecc66eeb3 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java @@ -52,11 +52,9 @@ import org.jspecify.annotations.Nullable; import org.springframework.cache.support.NullValue; -import org.springframework.core.KotlinDetector; import org.springframework.data.util.Lazy; import org.springframework.lang.Contract; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -68,6 +66,7 @@ * {@link JacksonObjectWriter}. * * @author Christoph Strobl + * @author Chris Bono * @see JacksonObjectReader * @see JacksonObjectWriter * @see ObjectMapper @@ -263,12 +262,11 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM */ public static class GenericJacksonJsonRedisSerializerBuilder>> { - private static final DefaultTyping DEFAULT_TYPING = DefaultTyping.NON_FINAL; - private final Supplier builderFactory; private boolean cacheNullValueSupportEnabled = false; - private @Nullable DefaultTyping defaultTyping = null; + private boolean defaultTypingEnabled; + private @Nullable DefaultTypingPredicate defaultTyping; private @Nullable String typePropertyName; private PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder() .allowIfBaseType(Object.class).allowIfSubType((ctx, clazz) -> true).build(); @@ -322,46 +320,46 @@ public GenericJacksonJsonRedisSerializerBuilder enableSpringCacheNullValueSup * @see https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data */ - @Contract("-> this") + @Contract("_ -> this") public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() { - withDefaultTyping(); + this.defaultTypingEnabled = true; return this; } /** * Enables * {@link JsonMapper.Builder#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String) - * default typing} without any type validation constraints. - *

- * WARNING: without restrictions of the {@link PolymorphicTypeValidator} deserialization is - * vulnerable to arbitrary code execution when reading from untrusted sources. + * default typing} using the given {@link PolymorphicTypeValidator}. * - * @param defaultTyping the default typing mode to use. * @return {@code this} builder. - * @since 4.0.3 - * @see https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data */ @Contract("_ -> this") - public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTyping defaultTyping) { + public GenericJacksonJsonRedisSerializerBuilder enableDefaultTyping(PolymorphicTypeValidator typeValidator) { + + typeValidator(typeValidator); + this.defaultTypingEnabled = true; - this.defaultTyping = defaultTyping; return this; } /** + * TODO: fix this javadoc * Enables * {@link JsonMapper.Builder#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String) - * default typing} using the given {@link PolymorphicTypeValidator}. + * default typing} without any type validation constraints. + *

+ * WARNING: without restrictions of the {@link PolymorphicTypeValidator} deserialization is + * vulnerable to arbitrary code execution when reading from untrusted sources. * + * @param defaultTyping the predicate that matches whether the type should have type info hints added. * @return {@code this} builder. */ @Contract("_ -> this") - public GenericJacksonJsonRedisSerializerBuilder enableDefaultTyping(PolymorphicTypeValidator typeValidator) { + public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTypingPredicate defaultTyping) { - typeValidator(typeValidator); - withDefaultTyping(); + this.defaultTypingEnabled = true; + this.defaultTyping = defaultTyping; return this; } @@ -395,15 +393,6 @@ public GenericJacksonJsonRedisSerializerBuilder typePropertyName(String typeP return this; } - /** - * Enable default typing using {@link DefaultTyping#NON_FINAL} if not already configured. - */ - private void withDefaultTyping() { - if (this.defaultTyping == null) { - defaultTyping(DEFAULT_TYPING); - } - } - /** * Configures the {@link JacksonObjectWriter}. * @@ -472,7 +461,7 @@ public GenericJacksonJsonRedisSerializer build() { })); } - if (defaultTyping != null) { + if (defaultTypingEnabled) { GenericJacksonJsonRedisSerializer.TypeResolverBuilder resolver = new GenericJacksonJsonRedisSerializer.TypeResolverBuilder( typeValidator, defaultTyping, JsonTypeInfo.As.PROPERTY, JsonTypeInfo.Id.CLASS, typePropertyName); @@ -628,12 +617,12 @@ public void setupModule(SetupContext context) { private static class TypeResolverBuilder extends DefaultTypeResolverBuilder { - private final DefaultTyping defaultTyping; + private final @Nullable DefaultTypingPredicate defaultTyping; - public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping defaultTyping, + public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, @Nullable DefaultTypingPredicate defaultTyping, JsonTypeInfo.As includeAs, JsonTypeInfo.Id idType, @Nullable String propertyName) { - super(subtypeValidator, defaultTyping, includeAs, idType, propertyName); + super(subtypeValidator, DefaultTyping.NON_FINAL, includeAs, idType, propertyName); this.defaultTyping = defaultTyping; } @@ -650,31 +639,19 @@ public DefaultTypeResolverBuilder withDefaultImpl(Class defaultImpl) { @Override public boolean useForType(JavaType javaType) { - if (javaType.isJavaLangObject()) { - return true; - } - - javaType = resolveArrayOrWrapper(javaType); - - if (javaType.isEnumType() && defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) { - return super.useForType(javaType); - } + JavaType resolvedType = resolveArrayOrWrapper(javaType); + Class rawClass = resolvedType.getRawClass(); - if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) { - return false; - } - - if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass()) - && javaType.getRawClass().getPackageName().startsWith("java")) { - return false; - } + DefaultTypingPredicate typingPredicate = defaultTyping != null ? defaultTyping : + DefaultTypingPredicate.defaults().build(); - if (defaultTyping != GenericJacksonJsonRedisSerializerBuilder.DEFAULT_TYPING) { - return super.useForType(javaType); - } + DefaultTypingPredicate.Action action = typingPredicate.test(rawClass); - // [databind#88] Should not apply to JSON tree models: - return !TreeNode.class.isAssignableFrom(javaType.getRawClass()); + return switch (action) { + case YES -> true; + case NO -> false; + case DONT_CARE -> !TreeNode.class.isAssignableFrom(rawClass); + }; } private JavaType resolveArrayOrWrapper(JavaType type) { diff --git a/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java b/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java new file mode 100644 index 0000000000..3e44694dc7 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.YES; +import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.NO; +import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.DONT_CARE; + +/** + * The default implementation of {@link DefaultTypingPredicate}, + * as created by the static factory methods. + * + * @see DefaultTypingPredicate#empty() + * @see DefaultTypingPredicate#defaults() + */ +final class StdDefaultTypingPredicate implements DefaultTypingPredicate { + + private final List typeSpecs = new ArrayList<>(); + + StdDefaultTypingPredicate(List typeSpecs) { + this.typeSpecs.addAll(typeSpecs); + } + + @Override + public Action test(Class clazz) { + + List results = typeSpecs.stream() + .filter((typeSpec) -> typeSpec.typeMatcher().test(clazz)) + .map(JavaTypeSpec::action) + .toList(); + + return results.isEmpty() ? DONT_CARE : results.get(results.size() - 1); + } + + static final class DefaultBuilder implements DefaultTypingPredicate.Builder { + + private final List typeSpecs = new ArrayList<>(); + + @Override + public DefaultTypingPredicate.Builder include(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, YES)); + return this; + } + + @Override + public DefaultTypingPredicate.Builder exclude(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, NO)); + return this; + } + + @Override + public DefaultTypingPredicate.Builder dontCare(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, DONT_CARE)); + return this; + } + + @Override + public DefaultTypingPredicate build() { + return new StdDefaultTypingPredicate(typeSpecs); + } + + } + + record JavaTypeSpec(Predicate> typeMatcher, DefaultTypingPredicate.Action action) { + } +} diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index ac7bdb4152..c23ce92aad 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -29,6 +29,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -44,6 +45,7 @@ import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; @@ -63,6 +65,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Chris Bono */ @SuppressWarnings("removal") class GenericJackson2JsonRedisSerializerUnitTests { @@ -403,40 +406,135 @@ void deserializesEnumFromBytes() { .isEqualTo(EnumType.TWO); } - @Test // GH-3306 - void serializesNonFinalIntoBytesWithTypeHint() { + @Test // GH-3309 + void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); + } + + @Test // GH-3309 + void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + .defaultTyping(defaultTyping).build(); + + assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.TWO); + } + + @Test // GH-3309 + void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(new String(serializer.serialize(EnumType.ONE))) .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); } - @Test // GH-3306 - void deserializesEnumFromBytesWithTypeHint() { + @Test // GH-3309 + void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + .defaultTyping(defaultTyping).build(); assertThat(serializer.deserialize( "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) .isEqualTo(EnumType.TWO); } - @Test // GH-3306 - void serializesRecordIntoBytesWithout() { + @Test // GH-3309 + void serializesRecordIntoBytesWithHintWhenTypingNotSpecified() { + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( + "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); + } + + @Test // GH-3309 + void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + + assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + + @Test // GH-3309 + void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS).build(); + .defaultTyping(defaultTyping).build(); - record Foo(String hello) { + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( + "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); + } - } + @Test // GH-3309 + void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + + @Test // GH-3309 + void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); } + @Test // GH-3309 + void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(serializer.deserialize("{\"hello\":\"world\"}".getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + @Test // GH-2396 void serializesJavaTimeIntoBytes() { @@ -816,4 +914,7 @@ static class WithJsr310 { @JsonSerialize(using = LocalDateSerializer.class) @JsonDeserialize(using = LocalDateDeserializer.class) private LocalDate myDate; } + + record Foo(String hello) { + } } diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java index 1f068a0f52..39dd3a7d76 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java @@ -60,6 +60,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author John Blum + * @author Chris Bono */ class GenericJacksonJsonRedisSerializerUnitTests { @@ -343,54 +344,147 @@ void deserializesUUIDFromBytes() { } @Test // GH-2396 - void serializesEnumIntoBytes() { + void serializesEnumIntoBytesWithoutHintWhenTypingNotSpecified() { assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); } @Test // GH-2396 - void deserializesEnumFromBytes() { + void deserializesEnumFromBytesWithoutHintWhenTypingNotSpecified() { + assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.TWO); + } + + @Test // GH-3309 + void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); + } + + @Test // GH-3309 + void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) .isEqualTo(EnumType.TWO); } - @Test // GH-3306 - void serializesEnumIntoBytesWithTypeHint() { + @Test // GH-3309 + void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { - it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); - }); + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(new String(serializer.serialize(EnumType.ONE))) .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); } - @Test // GH-3306 - void deserializesEnumFromBytesWithTypeHint() { + @Test // GH-3309 + void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { - it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); - }); + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(serializer.deserialize( "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) .isEqualTo(EnumType.TWO); } - @Test // GH-3306 - void serializesRecordIntoBytesWithoutHint() { + @Test // GH-3309 + void serializesRecordIntoBytesWithHintWhenTypingNotSpecified() { - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.create(it -> { - it.defaultTyping(DefaultTyping.NON_FINAL_AND_ENUMS); - }); + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .build(); - record Foo(String hello) { + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( + "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); + } - } + @Test // GH-3309 + void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .build(); + + assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + + @Test // GH-3309 + void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( + "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); + } + + @Test // GH-3309 + void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isRecord) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + + @Test // GH-3309 + void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); } + @Test // GH-3309 + void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { + + DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + .include(Class::isEnum) + .build(); + + GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() + .defaultTyping(defaultTyping).build(); + + assertThat(serializer.deserialize("{\"hello\":\"world\"}".getBytes(StandardCharsets.UTF_8), Foo.class)) + .isEqualTo(new Foo("world")); + } + @Test // GH-2396 void serializesJavaTimeIntoBytes() { @@ -704,4 +798,9 @@ static class WithJsr310 { @JsonSerialize(using = LocalDateSerializer.class) @JsonDeserialize(using = LocalDateDeserializer.class) private LocalDate myDate; } + + record Foo(String hello) { + + } + } From f10f9f59508ae6d3d7bf04a8cbcbbf62b7522772 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Wed, 1 Apr 2026 13:17:12 -0500 Subject: [PATCH 4/5] Rename predicate and action. --- .../redis/serializer/DefaultTypingPolicy.java | 126 ++++++++++++++++++ .../serializer/DefaultTypingPredicate.java | 80 ----------- .../GenericJackson2JsonRedisSerializer.java | 24 ++-- .../GenericJacksonJsonRedisSerializer.java | 22 +-- .../serializer/StdDefaultTypingPolicy.java | 98 ++++++++++++++ .../serializer/StdDefaultTypingPredicate.java | 83 ------------ ...cJackson2JsonRedisSerializerUnitTests.java | 18 ++- ...icJacksonJsonRedisSerializerUnitTests.java | 16 +-- 8 files changed, 263 insertions(+), 204 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultTypingPolicy.java delete mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPolicy.java delete mode 100644 src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPolicy.java b/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPolicy.java new file mode 100644 index 0000000000..e747523de9 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPolicy.java @@ -0,0 +1,126 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import java.lang.reflect.Modifier; +import java.util.function.Predicate; +import org.springframework.util.ClassUtils; + +/** + * Policy that defines whether to include automatic type information for Jackson + * for each serialized type. + *

+ * Provides a {@link Builder builder} to create a composite policy consisting of + * outcomes to apply for individual types. + *

+ * An example that uses the default policy and adds a rule for a custom type: + *

+ * DefaultTypingPolicy.defaults()
+ *     .include((clazz) -> clazz == Person.class)
+ *     .build();
+ * 
+ *

+ * This is a {@link FunctionalInterface} whose functional method is + * {@link #outcomeForType(Class)}. + * + * @author Chris Bono + * @since 4.1 + */ +public interface DefaultTypingPolicy { + + /** + * The outcome to apply for a given type. + */ + enum Outcome { + + /** Include type hints for the given type */ + INCLUDE_TYPE_HINT, + + /** Do not include type hints for the given type */ + EXCLUDE_TYPE_HINT, + + /** No opinion for the given type - fallback to the default logic */ + NO_OPINION; + } + + /** + * Determine the outcome to take for a particular type. + * @param clazz the type to check. + * @return the outcome for the type. + */ + Outcome outcomeForType(Class clazz); + + /** + * Obtain a builder with no defaults configured. + * @return a builder with no defaults configured. + */ + static DefaultTypingPolicy.Builder empty() { + return new StdDefaultTypingPolicy.DefaultBuilder(); + } + + /** + * Obtain a builder with defaults configured. + * @return a builder with defaults configured. + */ + static DefaultTypingPolicy.Builder defaults() { + + DefaultTypingPolicy.Builder builder = new StdDefaultTypingPolicy.DefaultBuilder(); + builder.include((clazz) -> clazz == Object.class); + builder.exclude((clazz) -> Modifier.isFinal(clazz.getModifiers()) && clazz.getPackageName().startsWith("java")); + builder.exclude(ClassUtils::isPrimitiveOrWrapper); + builder.include(Class::isEnum); + builder.include(Class::isRecord); + + return builder; + } + + /** + * A mutable builder for creating a {@link DefaultTypingPolicy}. + */ + interface Builder { + + /** + * Adds a rule that will return an {@link Outcome#INCLUDE_TYPE_HINT} outcome + * for matching types. + * @param typeMatcher the predicate to match the types. + * @return this. + */ + Builder include(Predicate> typeMatcher); + + /** + * Adds a rule that will return an {@link Outcome#EXCLUDE_TYPE_HINT} outcome + * for matching types. + * @param typeMatcher the predicate to match the types. + * @return this. + */ + Builder exclude(Predicate> typeMatcher); + + /** + * Adds a rule that will return an {@link Outcome#NO_OPINION} outcome + * for matching types. + * @param typeMatcher the predicate to match the types. + * @return this. + */ + Builder fallback(Predicate> typeMatcher); + + /** + * Build the policy. + * @return the policy. + */ + DefaultTypingPolicy build(); + } + +} diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java b/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java deleted file mode 100644 index 266cc9ef7c..0000000000 --- a/src/main/java/org/springframework/data/redis/serializer/DefaultTypingPredicate.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2026-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.redis.serializer; - -import java.lang.reflect.Modifier; -import java.util.function.Predicate; -import org.springframework.util.ClassUtils; - -/** - * @author Chris Bono - * @since 4.1 - */ -@FunctionalInterface -public interface DefaultTypingPredicate { - - enum Action { - YES, - NO, - DONT_CARE; - } - - /** - * Determine the action to take for a particular type. - * @param clazz the type to check. - * @return the action to take. - */ - Action test(Class clazz); - - /** - * Obtain a builder with no defaults configured. - * @return a builder with no defaults configured. - */ - static DefaultTypingPredicate.Builder empty() { - return new StdDefaultTypingPredicate.DefaultBuilder(); - } - - /** - * Obtain a builder with defaults configured. - * @return a builder with defaults configured. - */ - static DefaultTypingPredicate.Builder defaults() { - - DefaultTypingPredicate.Builder builder = new StdDefaultTypingPredicate.DefaultBuilder(); - builder.include((clazz) -> clazz == Object.class); - builder.exclude((clazz) -> Modifier.isFinal(clazz.getModifiers()) && clazz.getPackageName().startsWith("java")); - builder.exclude(ClassUtils::isPrimitiveOrWrapper); - builder.include(Class::isEnum); - builder.include(Class::isRecord); - - return builder; - } - - /** - * - */ - interface Builder { - - Builder include(Predicate> typeMatcher); - - Builder exclude(Predicate> typeMatcher); - - Builder dontCare(Predicate> typeMatcher); - - DefaultTypingPredicate build(); - } - -} diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index ff535a8054..5f0658e8f3 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -215,7 +215,7 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM }); } - private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTypingPredicate defaultTyping, + private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTypingPolicy defaultTyping, ObjectMapper objectMapper, @Nullable String typeHintPropertyName) { @@ -475,7 +475,7 @@ public static class GenericJackson2JsonRedisSerializerBuilder { private @Nullable Boolean defaultTypingEnabled; - private @Nullable DefaultTypingPredicate defaultTyping; + private @Nullable DefaultTypingPolicy defaultTyping; private boolean registerNullValueSerializer = true; @@ -507,7 +507,7 @@ public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTy * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}. * @since 4.0.4 */ - public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTypingPredicate defaultTyping) { + public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTypingPolicy defaultTyping) { this.defaultTypingEnabled = true; this.defaultTyping = defaultTyping; return this; @@ -640,15 +640,15 @@ public GenericJackson2JsonRedisSerializer build() { */ private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { - private final @Nullable DefaultTypingPredicate defaultTyping; + private final @Nullable DefaultTypingPolicy defaultTyping; - static TypeResolverBuilder forTyping(@Nullable DefaultTypingPredicate defaultTyping, ObjectMapper mapper) { + static TypeResolverBuilder forTyping(@Nullable DefaultTypingPolicy defaultTyping, ObjectMapper mapper) { return new TypeResolverBuilder( defaultTyping, mapper.getPolymorphicTypeValidator()); } - public TypeResolverBuilder(@Nullable DefaultTypingPredicate defaultTyping, PolymorphicTypeValidator polymorphicTypeValidator) { + public TypeResolverBuilder(@Nullable DefaultTypingPolicy defaultTyping, PolymorphicTypeValidator polymorphicTypeValidator) { super(DefaultTyping.EVERYTHING, polymorphicTypeValidator); this.defaultTyping = defaultTyping; } @@ -669,15 +669,15 @@ public boolean useForType(JavaType javaType) { JavaType resolvedType = resolveArrayOrWrapper(javaType); Class rawClass = resolvedType.getRawClass(); - DefaultTypingPredicate typingPredicate = defaultTyping != null ? defaultTyping : - DefaultTypingPredicate.defaults().build(); + DefaultTypingPolicy typingPredicate = defaultTyping != null ? defaultTyping : + DefaultTypingPolicy.defaults().build(); - DefaultTypingPredicate.Action action = typingPredicate.test(rawClass); + DefaultTypingPolicy.Outcome action = typingPredicate.outcomeForType(rawClass); return switch (action) { - case YES -> true; - case NO -> false; - case DONT_CARE -> !TreeNode.class.isAssignableFrom(rawClass); + case INCLUDE_TYPE_HINT -> true; + case EXCLUDE_TYPE_HINT -> false; + case NO_OPINION -> !TreeNode.class.isAssignableFrom(rawClass); }; } diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java index 7ecc66eeb3..ab7f9647fb 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java @@ -266,7 +266,7 @@ public static class GenericJacksonJsonRedisSerializerBuilder true).build(); @@ -320,7 +320,7 @@ public GenericJacksonJsonRedisSerializerBuilder enableSpringCacheNullValueSup * @see https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data */ - @Contract("_ -> this") + @Contract("-> this") public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() { this.defaultTypingEnabled = true; @@ -356,7 +356,7 @@ public GenericJacksonJsonRedisSerializerBuilder enableDefaultTyping(Polymorph * @return {@code this} builder. */ @Contract("_ -> this") - public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTypingPredicate defaultTyping) { + public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTypingPolicy defaultTyping) { this.defaultTypingEnabled = true; this.defaultTyping = defaultTyping; @@ -617,9 +617,9 @@ public void setupModule(SetupContext context) { private static class TypeResolverBuilder extends DefaultTypeResolverBuilder { - private final @Nullable DefaultTypingPredicate defaultTyping; + private final @Nullable DefaultTypingPolicy defaultTyping; - public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, @Nullable DefaultTypingPredicate defaultTyping, + public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, @Nullable DefaultTypingPolicy defaultTyping, JsonTypeInfo.As includeAs, JsonTypeInfo.Id idType, @Nullable String propertyName) { super(subtypeValidator, DefaultTyping.NON_FINAL, includeAs, idType, propertyName); @@ -642,15 +642,15 @@ public boolean useForType(JavaType javaType) { JavaType resolvedType = resolveArrayOrWrapper(javaType); Class rawClass = resolvedType.getRawClass(); - DefaultTypingPredicate typingPredicate = defaultTyping != null ? defaultTyping : - DefaultTypingPredicate.defaults().build(); + DefaultTypingPolicy typingPredicate = defaultTyping != null ? defaultTyping : + DefaultTypingPolicy.defaults().build(); - DefaultTypingPredicate.Action action = typingPredicate.test(rawClass); + DefaultTypingPolicy.Outcome action = typingPredicate.outcomeForType(rawClass); return switch (action) { - case YES -> true; - case NO -> false; - case DONT_CARE -> !TreeNode.class.isAssignableFrom(rawClass); + case INCLUDE_TYPE_HINT -> true; + case EXCLUDE_TYPE_HINT -> false; + case NO_OPINION -> !TreeNode.class.isAssignableFrom(rawClass); }; } diff --git a/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPolicy.java b/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPolicy.java new file mode 100644 index 0000000000..651df72b5f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPolicy.java @@ -0,0 +1,98 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import static org.springframework.data.redis.serializer.DefaultTypingPolicy.Outcome.INCLUDE_TYPE_HINT; +import static org.springframework.data.redis.serializer.DefaultTypingPolicy.Outcome.EXCLUDE_TYPE_HINT; +import static org.springframework.data.redis.serializer.DefaultTypingPolicy.Outcome.NO_OPINION; + +/** + * The default implementation of {@link DefaultTypingPolicy}, + * as created by the static factory methods. + * + * @see DefaultTypingPolicy#empty() + * @see DefaultTypingPolicy#defaults() + */ +final class StdDefaultTypingPolicy implements DefaultTypingPolicy { + + private final List typeSpecs = new ArrayList<>(); + + StdDefaultTypingPolicy(List typeSpecs) { + this.typeSpecs.addAll(typeSpecs); + } + + /** + * Determine the outcome to take for a particular type. + *

+ * Important: All rules are evaluated in the order they are + * added, with the last matching winning. If no outcome is matched + * for a particular type, the default {@link Outcome#NO_OPINION NO_OPINION} + * outcome is used. + * + * @param clazz the type to check. + * @return the outcome for the type. + */ + @Override + public Outcome outcomeForType(Class clazz) { + + List results = typeSpecs.stream() + .filter((typeSpec) -> typeSpec.typeMatcher().test(clazz)) + .map(JavaTypeSpec::outcome) + .toList(); + + return results.isEmpty() ? NO_OPINION : results.get(results.size() - 1); + } + + /** + * The default {@link DefaultTypingPolicy.Builder builder} implementation that + * builds an ordered list of rules to apply. + */ + static final class DefaultBuilder implements DefaultTypingPolicy.Builder { + + private final List typeSpecs = new ArrayList<>(); + + @Override + public DefaultTypingPolicy.Builder include(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, INCLUDE_TYPE_HINT)); + return this; + } + + @Override + public DefaultTypingPolicy.Builder exclude(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, EXCLUDE_TYPE_HINT)); + return this; + } + + @Override + public DefaultTypingPolicy.Builder fallback(Predicate> typeMatcher) { + typeSpecs.add(new JavaTypeSpec(typeMatcher, NO_OPINION)); + return this; + } + + @Override + public DefaultTypingPolicy build() { + return new StdDefaultTypingPolicy(typeSpecs); + } + + } + + record JavaTypeSpec(Predicate> typeMatcher, Outcome outcome) { + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java b/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java deleted file mode 100644 index 3e44694dc7..0000000000 --- a/src/main/java/org/springframework/data/redis/serializer/StdDefaultTypingPredicate.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2026-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.redis.serializer; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; - -import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.YES; -import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.NO; -import static org.springframework.data.redis.serializer.DefaultTypingPredicate.Action.DONT_CARE; - -/** - * The default implementation of {@link DefaultTypingPredicate}, - * as created by the static factory methods. - * - * @see DefaultTypingPredicate#empty() - * @see DefaultTypingPredicate#defaults() - */ -final class StdDefaultTypingPredicate implements DefaultTypingPredicate { - - private final List typeSpecs = new ArrayList<>(); - - StdDefaultTypingPredicate(List typeSpecs) { - this.typeSpecs.addAll(typeSpecs); - } - - @Override - public Action test(Class clazz) { - - List results = typeSpecs.stream() - .filter((typeSpec) -> typeSpec.typeMatcher().test(clazz)) - .map(JavaTypeSpec::action) - .toList(); - - return results.isEmpty() ? DONT_CARE : results.get(results.size() - 1); - } - - static final class DefaultBuilder implements DefaultTypingPredicate.Builder { - - private final List typeSpecs = new ArrayList<>(); - - @Override - public DefaultTypingPredicate.Builder include(Predicate> typeMatcher) { - typeSpecs.add(new JavaTypeSpec(typeMatcher, YES)); - return this; - } - - @Override - public DefaultTypingPredicate.Builder exclude(Predicate> typeMatcher) { - typeSpecs.add(new JavaTypeSpec(typeMatcher, NO)); - return this; - } - - @Override - public DefaultTypingPredicate.Builder dontCare(Predicate> typeMatcher) { - typeSpecs.add(new JavaTypeSpec(typeMatcher, DONT_CARE)); - return this; - } - - @Override - public DefaultTypingPredicate build() { - return new StdDefaultTypingPredicate(typeSpecs); - } - - } - - record JavaTypeSpec(Predicate> typeMatcher, DefaultTypingPredicate.Action action) { - } -} diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index c23ce92aad..8bfda4260e 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -29,7 +29,6 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Predicate; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -45,7 +44,6 @@ import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; @@ -409,7 +407,7 @@ void deserializesEnumFromBytes() { @Test // GH-3309 void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -422,7 +420,7 @@ void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -436,7 +434,7 @@ void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -450,7 +448,7 @@ void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -483,7 +481,7 @@ void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { @Test // GH-3309 void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -497,7 +495,7 @@ void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -511,7 +509,7 @@ void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -524,7 +522,7 @@ void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java index 39dd3a7d76..808ee5f36a 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java @@ -357,7 +357,7 @@ void deserializesEnumFromBytesWithoutHintWhenTypingNotSpecified() { @Test // GH-3309 void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -370,7 +370,7 @@ void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -384,7 +384,7 @@ void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -398,7 +398,7 @@ void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -433,7 +433,7 @@ void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { @Test // GH-3309 void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -447,7 +447,7 @@ void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isRecord) .build(); @@ -461,7 +461,7 @@ void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { @Test // GH-3309 void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); @@ -474,7 +474,7 @@ void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { @Test // GH-3309 void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { - DefaultTypingPredicate defaultTyping = DefaultTypingPredicate.empty() + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() .include(Class::isEnum) .build(); From a4ef0364a96ba8318fd7b34c2a7ab120d12cea7e Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Wed, 1 Apr 2026 14:24:05 -0500 Subject: [PATCH 5/5] Update tests. --- .../GenericJackson2JsonRedisSerializer.java | 4 +- .../GenericJacksonJsonRedisSerializer.java | 6 +- .../DefaultTypingPolicyUnitTests.java | 111 ++++++++++++++ ...cJackson2JsonRedisSerializerUnitTests.java | 138 ++++-------------- ...icJacksonJsonRedisSerializerUnitTests.java | 136 ++++------------- 5 files changed, 167 insertions(+), 228 deletions(-) create mode 100644 src/test/java/org/springframework/data/redis/serializer/DefaultTypingPolicyUnitTests.java diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index 5f0658e8f3..05872a1dcd 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -672,9 +672,7 @@ public boolean useForType(JavaType javaType) { DefaultTypingPolicy typingPredicate = defaultTyping != null ? defaultTyping : DefaultTypingPolicy.defaults().build(); - DefaultTypingPolicy.Outcome action = typingPredicate.outcomeForType(rawClass); - - return switch (action) { + return switch (typingPredicate.outcomeForType(rawClass)) { case INCLUDE_TYPE_HINT -> true; case EXCLUDE_TYPE_HINT -> false; case NO_OPINION -> !TreeNode.class.isAssignableFrom(rawClass); diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java index ab7f9647fb..c8885e3421 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializer.java @@ -642,12 +642,10 @@ public boolean useForType(JavaType javaType) { JavaType resolvedType = resolveArrayOrWrapper(javaType); Class rawClass = resolvedType.getRawClass(); - DefaultTypingPolicy typingPredicate = defaultTyping != null ? defaultTyping : + DefaultTypingPolicy typingPolicy = defaultTyping != null ? defaultTyping : DefaultTypingPolicy.defaults().build(); - DefaultTypingPolicy.Outcome action = typingPredicate.outcomeForType(rawClass); - - return switch (action) { + return switch (typingPolicy.outcomeForType(rawClass)) { case INCLUDE_TYPE_HINT -> true; case EXCLUDE_TYPE_HINT -> false; case NO_OPINION -> !TreeNode.class.isAssignableFrom(rawClass); diff --git a/src/test/java/org/springframework/data/redis/serializer/DefaultTypingPolicyUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/DefaultTypingPolicyUnitTests.java new file mode 100644 index 0000000000..82cdb5ef2b --- /dev/null +++ b/src/test/java/org/springframework/data/redis/serializer/DefaultTypingPolicyUnitTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import java.util.Arrays; +import java.util.Collection; +import org.assertj.core.util.introspection.ClassUtils; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.serializer.DefaultTypingPolicy.Outcome; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for {@link DefaultTypingPolicy}. + * + * @author Chris Bono + */ +class DefaultTypingPolicyUnitTests { + + @Test + void emptyPolicyReturnsNoOpinionForAllTypes() { + + DefaultTypingPolicy policy = DefaultTypingPolicy.empty().build(); + + assertThat(policy.outcomeForType(String.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(Collection.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(ClassType.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(RecordType.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(EnumType.class)).isEqualTo(Outcome.NO_OPINION); + } + + @Test + void policyWithBasicRules() { + + DefaultTypingPolicy policy = DefaultTypingPolicy.empty() + .include(Class::isEnum) + .include(Class::isRecord) + .exclude(ClassUtils::isPrimitiveOrWrapper) + .exclude(ClassUtils::isInJavaLangPackage) + .build(); + + assertThat(policy.outcomeForType(ClassType.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(EnumType.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(RecordType.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(int.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(Integer.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(String.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + } + + @Test + void policyRulesCanBeOverridden() { + + DefaultTypingPolicy policy = DefaultTypingPolicy.empty() + .include(Class::isEnum) + .include(Class::isRecord) + .exclude(Class::isEnum) + .build(); + + assertThat(policy.outcomeForType(ClassType.class)).isEqualTo(Outcome.NO_OPINION); + assertThat(policy.outcomeForType(EnumType.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(RecordType.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + } + + @Test + void defaultPolicyRulesAsExpected() { + + DefaultTypingPolicy policy = DefaultTypingPolicy.defaults().build(); + + // java.lang.Object included + assertThat(policy.outcomeForType(Object.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + + // Final java.* type excluded + assertThat(policy.outcomeForType(Arrays.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + + // Non-final java.* type no opinion + assertThat(policy.outcomeForType(RuntimeException.class)).isEqualTo(Outcome.NO_OPINION); + + // Enums included + assertThat(policy.outcomeForType(EnumType.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + + // Records included + assertThat(policy.outcomeForType(RecordType.class)).isEqualTo(Outcome.INCLUDE_TYPE_HINT); + + // Primitive and wrappers excluded + assertThat(policy.outcomeForType(int.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + assertThat(policy.outcomeForType(Integer.class)).isEqualTo(Outcome.EXCLUDE_TYPE_HINT); + } + + class ClassType { + } + + record RecordType(String name) { + } + + enum EnumType { + ONE, TWO; + } +} diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index 8bfda4260e..eccab08eb2 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -387,149 +387,65 @@ void deserializesUUIDFromBytes() { assertThat(deserializedUuid).isEqualTo(UUID.fromString("730145fe-324d-4fb1-b12f-60b89a045730")); } - @Test // GH-2396 - void serializesEnumIntoBytes() { + @Test // GH-3306 + void serializesEnumWithHintByDefault() { GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); - assertThat(serializer.serialize(EnumType.ONE)).isEqualTo(("\"ONE\"").getBytes(StandardCharsets.UTF_8)); - } - - @Test // GH-2396 - void deserializesEnumFromBytes() { + String expectedSerialized = "[\"%s\",\"ONE\"]"; - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + assertThat(new String(serializer.serialize(EnumType.ONE))) + .isEqualTo(expectedSerialized.formatted(EnumType.class.getName())); - assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); + assertThat(serializer.deserialize( + expectedSerialized.formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.ONE); } - @Test // GH-3309 - void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + @Test // GH-3306 + void serializesEnumWithoutHintWhenDefaultsOverridden() { - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults() + .exclude(Class::isEnum) .build(); GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() .defaultTyping(defaultTyping).build(); assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); - } - - @Test // GH-3309 - void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); - } - - @Test // GH-3309 - void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(new String(serializer.serialize(EnumType.ONE))) - .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); - } - - @Test // GH-3309 - void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - assertThat(serializer.deserialize( - "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); + assertThat(serializer.deserialize("\"ONE\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.ONE); } - @Test // GH-3309 - void serializesRecordIntoBytesWithHintWhenTypingNotSpecified() { + @Test // GH-3306 + void serializesRecordWithHintByDefault() { GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( - "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); - } - - @Test // GH-3309 - void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { + String expectedSerialized = "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()); - GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized); - assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class)) .isEqualTo(new Foo("world")); } - @Test // GH-3309 - void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { + @Test // GH-3306 + void serializesRecordWithoutHintWhenDefaultsOverridden() { - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults() + .exclude(Class::isRecord) .build(); GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() .defaultTyping(defaultTyping).build(); - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( - "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); - } - - @Test // GH-3309 - void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { + String expectedSerialized = "{\"hello\":\"world\"}"; - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) - .isEqualTo(new Foo("world")); - } - - @Test // GH-3309 - void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); - } - - @Test // GH-3309 - void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized); - assertThat(serializer.deserialize("{\"hello\":\"world\"}".getBytes(StandardCharsets.UTF_8), Foo.class)) + assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class)) .isEqualTo(new Foo("world")); } @@ -547,7 +463,7 @@ void serializesJavaTimeIntoBytes() { } @Test // GH-2396 - void deserializesJavaTimeFrimBytes() { + void deserializesJavaTimeFromBytes() { GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java index 808ee5f36a..065a4ec3be 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJacksonJsonRedisSerializerUnitTests.java @@ -343,145 +343,61 @@ void deserializesUUIDFromBytes() { assertThat(deserializedUuid).isEqualTo(UUID.fromString("730145fe-324d-4fb1-b12f-60b89a045730")); } - @Test // GH-2396 - void serializesEnumIntoBytesWithoutHintWhenTypingNotSpecified() { - assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); - } - - @Test // GH-2396 - void deserializesEnumFromBytesWithoutHintWhenTypingNotSpecified() { - assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); - } - - @Test // GH-3309 - void serializesEnumIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) - .build(); - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); - } + @Test // GH-3306 + void serializesEnumWithHintByDefault() { - @Test // GH-3309 - void deserializesEnumFromBytesWithoutHintWhenNonMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) - .build(); - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); - - assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); - } - - @Test // GH-3309 - void serializesEnumIntoBytesWithHintWhenMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); + String expectedSerialized = "[\"%s\",\"ONE\"]"; assertThat(new String(serializer.serialize(EnumType.ONE))) - .isEqualTo("[\"%s\",\"ONE\"]".formatted(EnumType.class.getName())); - } - - @Test // GH-3309 - void deserializesEnumFromBytesWithHintWhenMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); + .isEqualTo(expectedSerialized.formatted(EnumType.class.getName())); assertThat(serializer.deserialize( - "[\"%s\",\"TWO\"]".formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) - .isEqualTo(EnumType.TWO); + expectedSerialized.formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.ONE); } - @Test // GH-3309 - void serializesRecordIntoBytesWithHintWhenTypingNotSpecified() { + @Test // GH-3306 + void serializesEnumWithoutHintWhenDefaultsOverridden() { - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .build(); - - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( - "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); - } - - @Test // GH-3309 - void deserializesRecordFromBytesWithHintWhenTypingNotSpecified() { - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .build(); - - assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) - .isEqualTo(new Foo("world")); - } - - @Test // GH-3309 - void serializesRecordIntoBytesWithHintWhenMatchingTypingSpecified() { - - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults() + .exclude(Class::isEnum) .build(); GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() .defaultTyping(defaultTyping).build(); - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo( - "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName())); + assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\"")); + + assertThat(serializer.deserialize("\"ONE\"".getBytes(StandardCharsets.UTF_8), EnumType.class)) + .isEqualTo(EnumType.ONE); } - @Test // GH-3309 - void deserializesRecordFromBytesWithHintWhenMatchingTypingSpecified() { + @Test // GH-3306 + void serializesRecordWithHintByDefault() { - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isRecord) - .build(); + String expectedSerialized = "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()); - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized); - assertThat(serializer.deserialize("{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName()).getBytes(StandardCharsets.UTF_8), Foo.class)) + assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class)) .isEqualTo(new Foo("world")); } - @Test // GH-3309 - void serializesRecordIntoBytesWithoutHintWhenNonMatchingTypingSpecified() { + @Test // GH-3306 + void serializesRecordWithoutHintWhenDefaultsOverridden() { - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) + DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults() + .exclude(Class::isRecord) .build(); GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() .defaultTyping(defaultTyping).build(); - assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo("{\"hello\":\"world\"}"); - } - - @Test // GH-3309 - void deserializesRecordBytesWithoutHintWhenNonMatchingTypingSpecified() { + String expectedSerialized = "{\"hello\":\"world\"}"; - DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.empty() - .include(Class::isEnum) - .build(); - - GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() - .defaultTyping(defaultTyping).build(); + assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized); - assertThat(serializer.deserialize("{\"hello\":\"world\"}".getBytes(StandardCharsets.UTF_8), Foo.class)) + assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class)) .isEqualTo(new Foo("world")); }