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
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/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
index 8865b6359e..05872a1dcd 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,7 +134,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName,
registerNullValueSerializer(this.mapper, typeHintPropertyName);
- this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(getObjectMapper(), typeHintPropertyName));
+ this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(null, getObjectMapper(), typeHintPropertyName));
}
/**
@@ -216,10 +215,12 @@ private static Lazy getConfiguredTypeDeserializationPropertyName(ObjectM
});
}
- private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(ObjectMapper objectMapper,
+ private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(@Nullable DefaultTypingPolicy 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)) {
@@ -472,7 +473,9 @@ public static class GenericJackson2JsonRedisSerializerBuilder {
private @Nullable ObjectMapper objectMapper;
- private @Nullable Boolean defaultTyping;
+ private @Nullable Boolean defaultTypingEnabled;
+
+ private @Nullable DefaultTypingPolicy defaultTyping;
private boolean registerNullValueSerializer = true;
@@ -490,6 +493,22 @@ private GenericJackson2JsonRedisSerializerBuilder() {}
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) {
+ this.defaultTypingEnabled = defaultTyping;
+ this.defaultTyping = 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 predicate that matches whether the type should have type info hints added.
+ * @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
+ * @since 4.0.4
+ */
+ public GenericJackson2JsonRedisSerializerBuilder defaultTyping(DefaultTypingPolicy defaultTyping) {
+ this.defaultTypingEnabled = true;
this.defaultTyping = defaultTyping;
return this;
}
@@ -599,9 +618,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 +640,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 @Nullable DefaultTypingPolicy defaultTyping;
+
+ static TypeResolverBuilder forTyping(@Nullable DefaultTypingPolicy defaultTyping, ObjectMapper mapper) {
+ return new TypeResolverBuilder(
+ defaultTyping,
+ mapper.getPolymorphicTypeValidator());
}
- public TypeResolverBuilder(DefaultTyping typing, PolymorphicTypeValidator polymorphicTypeValidator) {
- super(typing, polymorphicTypeValidator);
+ public TypeResolverBuilder(@Nullable DefaultTypingPolicy defaultTyping, PolymorphicTypeValidator polymorphicTypeValidator) {
+ super(DefaultTyping.EVERYTHING, polymorphicTypeValidator);
+ this.defaultTyping = defaultTyping;
}
@Override
@@ -640,23 +666,17 @@ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class> defaultI
@Override
public boolean useForType(JavaType javaType) {
- if (javaType.isJavaLangObject()) {
- return true;
- }
-
- javaType = resolveArrayOrWrapper(javaType);
-
- if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
- return false;
- }
+ JavaType resolvedType = resolveArrayOrWrapper(javaType);
+ Class> rawClass = resolvedType.getRawClass();
- if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass())
- && javaType.getRawClass().getPackageName().startsWith("java")) {
- return false;
- }
+ DefaultTypingPolicy typingPredicate = defaultTyping != null ? defaultTyping :
+ DefaultTypingPolicy.defaults().build();
- // [databind#88] Should not apply to JSON tree models:
- return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
+ return switch (typingPredicate.outcomeForType(rawClass)) {
+ case INCLUDE_TYPE_HINT -> true;
+ case EXCLUDE_TYPE_HINT -> false;
+ case NO_OPINION -> !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 e486a3f7a5..c8885e3421 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
@@ -266,7 +265,8 @@ public static class GenericJacksonJsonRedisSerializerBuilder builderFactory;
private boolean cacheNullValueSupportEnabled = false;
- private boolean defaultTyping = false;
+ private boolean defaultTypingEnabled;
+ private @Nullable DefaultTypingPolicy defaultTyping;
private @Nullable String typePropertyName;
private PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class).allowIfSubType((ctx, clazz) -> true).build();
@@ -323,7 +323,7 @@ public GenericJacksonJsonRedisSerializerBuilder enableSpringCacheNullValueSup
@Contract("-> this")
public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() {
- this.defaultTyping = true;
+ this.defaultTypingEnabled = true;
return this;
}
@@ -338,8 +338,29 @@ public GenericJacksonJsonRedisSerializerBuilder enableUnsafeDefaultTyping() {
public GenericJacksonJsonRedisSerializerBuilder enableDefaultTyping(PolymorphicTypeValidator typeValidator) {
typeValidator(typeValidator);
+ this.defaultTypingEnabled = true;
+
+ return this;
+ }
+
+ /**
+ * TODO: fix this javadoc
+ * 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 predicate that matches whether the type should have type info hints added.
+ * @return {@code this} builder.
+ */
+ @Contract("_ -> this")
+ public GenericJacksonJsonRedisSerializerBuilder defaultTyping(DefaultTypingPolicy defaultTyping) {
+
+ this.defaultTypingEnabled = true;
+ this.defaultTyping = defaultTyping;
- this.defaultTyping = true;
return this;
}
@@ -440,10 +461,10 @@ public GenericJacksonJsonRedisSerializer build() {
}));
}
- if (defaultTyping) {
+ if (defaultTypingEnabled) {
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 +617,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 @Nullable DefaultTypingPolicy defaultTyping;
- public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, DefaultTyping t, JsonTypeInfo.As includeAs,
+ public TypeResolverBuilder(PolymorphicTypeValidator subtypeValidator, @Nullable DefaultTypingPolicy defaultTyping,
+ JsonTypeInfo.As includeAs,
JsonTypeInfo.Id idType, @Nullable String propertyName) {
- super(subtypeValidator, t, includeAs, idType, propertyName);
+ super(subtypeValidator, DefaultTyping.NON_FINAL, includeAs, idType, propertyName);
+ this.defaultTyping = defaultTyping;
}
@Override
@@ -622,23 +639,17 @@ public DefaultTypeResolverBuilder withDefaultImpl(Class> defaultImpl) {
@Override
public boolean useForType(JavaType javaType) {
- if (javaType.isJavaLangObject()) {
- return true;
- }
-
- javaType = resolveArrayOrWrapper(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;
- }
+ DefaultTypingPolicy typingPolicy = defaultTyping != null ? defaultTyping :
+ DefaultTypingPolicy.defaults().build();
- // [databind#88] Should not apply to JSON tree models:
- return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
+ return switch (typingPolicy.outcomeForType(rawClass)) {
+ case INCLUDE_TYPE_HINT -> true;
+ case EXCLUDE_TYPE_HINT -> false;
+ case NO_OPINION -> !TreeNode.class.isAssignableFrom(rawClass);
+ };
}
private JavaType resolveArrayOrWrapper(JavaType type) {
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/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 c959637650..eccab08eb2 100644
--- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
@@ -63,7 +63,9 @@
* @author Christoph Strobl
* @author Mark Paluch
* @author John Blum
+ * @author Chris Bono
*/
+@SuppressWarnings("removal")
class GenericJackson2JsonRedisSerializerUnitTests {
private static final SimpleObject SIMPLE_OBJECT = new SimpleObject(1L);
@@ -385,21 +387,66 @@ 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));
+ String expectedSerialized = "[\"%s\",\"ONE\"]";
+
+ assertThat(new String(serializer.serialize(EnumType.ONE)))
+ .isEqualTo(expectedSerialized.formatted(EnumType.class.getName()));
+
+ assertThat(serializer.deserialize(
+ expectedSerialized.formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class))
+ .isEqualTo(EnumType.ONE);
}
- @Test // GH-2396
- void deserializesEnumFromBytes() {
+ @Test // GH-3306
+ void serializesEnumWithoutHintWhenDefaultsOverridden() {
+
+ DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults()
+ .exclude(Class::isEnum)
+ .build();
+
+ GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
+ .defaultTyping(defaultTyping).build();
+
+ assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\""));
+
+ assertThat(serializer.deserialize("\"ONE\"".getBytes(StandardCharsets.UTF_8), EnumType.class))
+ .isEqualTo(EnumType.ONE);
+ }
+
+ @Test // GH-3306
+ void serializesRecordWithHintByDefault() {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
- assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class))
- .isEqualTo(EnumType.TWO);
+ String expectedSerialized = "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName());
+
+ assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized);
+
+ assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class))
+ .isEqualTo(new Foo("world"));
+ }
+
+ @Test // GH-3306
+ void serializesRecordWithoutHintWhenDefaultsOverridden() {
+
+ DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults()
+ .exclude(Class::isRecord)
+ .build();
+
+ GenericJackson2JsonRedisSerializer serializer = GenericJackson2JsonRedisSerializer.builder()
+ .defaultTyping(defaultTyping).build();
+
+ String expectedSerialized = "{\"hello\":\"world\"}";
+
+ assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized);
+
+ assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class))
+ .isEqualTo(new Foo("world"));
}
@Test // GH-2396
@@ -416,7 +463,7 @@ void serializesJavaTimeIntoBytes() {
}
@Test // GH-2396
- void deserializesJavaTimeFrimBytes() {
+ void deserializesJavaTimeFromBytes() {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
@@ -781,4 +828,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 a3cec6c3d4..065a4ec3be 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 {
@@ -342,21 +343,62 @@ void deserializesUUIDFromBytes() {
assertThat(deserializedUuid).isEqualTo(UUID.fromString("730145fe-324d-4fb1-b12f-60b89a045730"));
}
- @Test // GH-2396
- void serializesEnumIntoBytes() {
+ @Test // GH-3306
+ void serializesEnumWithHintByDefault() {
- GenericJacksonJsonRedisSerializer serializer = this.serializer;
+ String expectedSerialized = "[\"%s\",\"ONE\"]";
+
+ assertThat(new String(serializer.serialize(EnumType.ONE)))
+ .isEqualTo(expectedSerialized.formatted(EnumType.class.getName()));
- assertThat(serializer.serialize(EnumType.ONE)).isEqualTo(("\"ONE\"").getBytes(StandardCharsets.UTF_8));
+ assertThat(serializer.deserialize(
+ expectedSerialized.formatted(EnumType.class.getName()).getBytes(StandardCharsets.UTF_8), EnumType.class))
+ .isEqualTo(EnumType.ONE);
}
- @Test // GH-2396
- void deserializesEnumFromBytes() {
+ @Test // GH-3306
+ void serializesEnumWithoutHintWhenDefaultsOverridden() {
- GenericJacksonJsonRedisSerializer serializer = this.serializer;
+ DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults()
+ .exclude(Class::isEnum)
+ .build();
+
+ GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder()
+ .defaultTyping(defaultTyping).build();
+
+ assertThat(new String(serializer.serialize(EnumType.ONE))).isEqualTo(("\"ONE\""));
+
+ assertThat(serializer.deserialize("\"ONE\"".getBytes(StandardCharsets.UTF_8), EnumType.class))
+ .isEqualTo(EnumType.ONE);
+ }
+
+ @Test // GH-3306
+ void serializesRecordWithHintByDefault() {
+
+ String expectedSerialized = "{\"@class\":\"%s\",\"hello\":\"world\"}".formatted(Foo.class.getName());
- assertThat(serializer.deserialize("\"TWO\"".getBytes(StandardCharsets.UTF_8), EnumType.class))
- .isEqualTo(EnumType.TWO);
+ assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized);
+
+ assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class))
+ .isEqualTo(new Foo("world"));
+ }
+
+ @Test // GH-3306
+ void serializesRecordWithoutHintWhenDefaultsOverridden() {
+
+ DefaultTypingPolicy defaultTyping = DefaultTypingPolicy.defaults()
+ .exclude(Class::isRecord)
+ .build();
+
+ GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder()
+ .defaultTyping(defaultTyping).build();
+
+ String expectedSerialized = "{\"hello\":\"world\"}";
+
+ assertThat(new String(serializer.serialize(new Foo("world")))).isEqualTo(expectedSerialized);
+
+ assertThat(serializer.deserialize(expectedSerialized.getBytes(StandardCharsets.UTF_8), Foo.class))
+ .isEqualTo(new Foo("world"));
}
@Test // GH-2396
@@ -672,4 +714,9 @@ static class WithJsr310 {
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class) private LocalDate myDate;
}
+
+ record Foo(String hello) {
+
+ }
+
}