Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.0-GH-3306-SNAPSHOT</version>

<name>Spring Data Redis</name>
<description>Spring Data module for Redis</description>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Provides a {@link Builder builder} to create a composite policy consisting of
* outcomes to apply for individual types.
* <p>
* An example that uses the default policy and adds a rule for a custom type:
* <pre class="code">
* DefaultTypingPolicy.defaults()
* .include((clazz) -> clazz == Person.class)
* .build();
* </pre>
* <p>
* 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<Class<?>> 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<Class<?>> 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<Class<?>> typeMatcher);

/**
* Build the policy.
* @return the policy.
*/
DefaultTypingPolicy build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -216,10 +215,12 @@ private static Lazy<String> 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)) {
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Loading
Loading