Skip to content

Introduce JacksonObjectReader and JacksonObjectWriter function interfaces to customize JSON (de)serialization #2332

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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>3.0.0-SNAPSHOT</version>
<version>3.0.0-GH-2322-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
Expand Up @@ -16,27 +16,36 @@
package org.springframework.data.redis.serializer;

import java.io.IOException;
import java.util.Collections;
import java.util.function.Supplier;

import org.springframework.cache.support.NullValue;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;

/**
* Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing.
* <p>
* JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective
* {@link JacksonObjectWriter}.
*
* @author Christoph Strobl
* @author Mark Paluch
Expand All @@ -47,6 +56,14 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec

private final ObjectMapper mapper;

private final JacksonObjectReader reader;

private final JacksonObjectWriter writer;

private final Lazy<Boolean> defaultTypingEnabled;

private final TypeResolver typeResolver;

/**
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
*/
Expand All @@ -59,13 +76,30 @@ public GenericJackson2JsonRedisSerializer() {
* given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
* {@link JsonTypeInfo.Id#CLASS} will be used.
*
* @param classPropertyTypeName Name of the JSON property holding type information. Can be {@literal null}.
* @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
*/
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create());
}

/**
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the
* given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
* {@link JsonTypeInfo.Id#CLASS} will be used.
*
* @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
* @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
* @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
* @since 3.0
*/
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName, JacksonObjectReader reader,
JacksonObjectWriter writer) {

this(new ObjectMapper());
this(new ObjectMapper(), reader, writer, classPropertyTypeName);

// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
Expand All @@ -87,9 +121,57 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
* @param mapper must not be {@literal null}.
*/
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create());
}

/**
* Setting a custom-configured {@link ObjectMapper} is one way to take further control of the JSON serialization
* process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for
* specific types.
*
* @param mapper must not be {@literal null}.
* @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
* @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
* @since 3.0
*/
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader,
JacksonObjectWriter writer) {
this(mapper, reader, writer, null);
}

private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader,
JacksonObjectWriter writer, @Nullable String typeHintPropertyName) {

Assert.notNull(mapper, "ObjectMapper must not be null!");
Assert.notNull(reader, "Reader must not be null!");
Assert.notNull(writer, "Writer must not be null!");

this.mapper = mapper;
this.reader = reader;
this.writer = writer;

this.defaultTypingEnabled = Lazy.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null);

Supplier<String> typeHintPropertyNameSupplier;

if (typeHintPropertyName == null) {

typeHintPropertyNameSupplier = Lazy.of(() -> {
if (defaultTypingEnabled.get()) {
return null;
}

return mapper.getDeserializationConfig().getDefaultTyper(null)
.buildTypeDeserializer(mapper.getDeserializationConfig(),
mapper.getTypeFactory().constructType(Object.class), Collections.emptyList())
.getPropertyName();

}).or("@class");
} else {
typeHintPropertyNameSupplier = () -> typeHintPropertyName;
}

this.typeResolver = new TypeResolver(Lazy.of(mapper::getTypeFactory), typeHintPropertyNameSupplier);
}

/**
Expand All @@ -116,8 +198,8 @@ public byte[] serialize(@Nullable Object source) throws SerializationException {
}

try {
return mapper.writeValueAsBytes(source);
} catch (JsonProcessingException e) {
return writer.write(mapper, source);
} catch (IOException e) {
throw new SerializationException("Could not write JSON: " + e.getMessage(), e);
}
}
Expand All @@ -134,6 +216,7 @@ public Object deserialize(@Nullable byte[] source) throws SerializationException
* @throws SerializationException
*/
@Nullable
@SuppressWarnings("unchecked")
public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {

Assert.notNull(type,
Expand All @@ -144,12 +227,53 @@ public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws Serializ
}

try {
return mapper.readValue(source, type);
return (T) reader.read(mapper, source, resolveType(source, type));
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
}

protected JavaType resolveType(byte[] source, Class<?> type) throws IOException {

if (!type.equals(Object.class) || !defaultTypingEnabled.get()) {
return typeResolver.constructType(type);
}

return typeResolver.resolveType(source, type);
}

static class TypeResolver {

// need a separate instance to bypass class hint checks
private final ObjectMapper mapper = new ObjectMapper();

private final Supplier<TypeFactory> typeFactory;
private final Supplier<String> hintName;

public TypeResolver(Supplier<TypeFactory> typeFactory, Supplier<String> hintName) {

this.typeFactory = typeFactory;
this.hintName = hintName;
}

protected JavaType constructType(Class<?> type) {
return typeFactory.get().constructType(type);
}

protected JavaType resolveType(byte[] source, Class<?> type) throws IOException {

JsonNode root = mapper.readTree(source);
JsonNode jsonNode = root.get(hintName.get());

if (jsonNode instanceof TextNode && jsonNode.asText() != null) {
return typeFactory.get().constructFromCanonical(jsonNode.asText());
}

return constructType(type);
}

}

/**
* {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of
* {@link NullValue}.
Expand All @@ -172,8 +296,7 @@ private static class NullValueSerializer extends StdSerializer<NullValue> {
}

@Override
public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {

jgen.writeStartObject();
jgen.writeStringField(classIdentifier, NullValue.class.getName());
Expand All @@ -186,4 +309,5 @@ public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProv
serialize(value, gen, serializers);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,98 @@
* <a href="https://github.com/FasterXML/jackson-core">Jackson's</a> and
* <a href="https://github.com/FasterXML/jackson-databind">Jackson Databind</a> {@link ObjectMapper}.
* <p>
* This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances.
* This serializer can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances.
* <b>Note:</b>Null objects are serialized as empty arrays and vice versa.
* <p>
* JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective
* {@link JacksonObjectWriter}.
*
* @author Thomas Darimont
* @author Mark Paluch
* @since 1.2
*/
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {

/**
* @deprecated since 3.0 for removal.
*/
@Deprecated(since = "3.0", forRemoval = true) //
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

private final JavaType javaType;

private ObjectMapper objectMapper = new ObjectMapper();
private ObjectMapper mapper;

private final JacksonObjectReader reader;

private final JacksonObjectWriter writer;

/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}.
*
* @param type
* @param type must not be {@literal null}.
*/
public Jackson2JsonRedisSerializer(Class<T> type) {
this.javaType = getJavaType(type);
this(new ObjectMapper(), type);
}

/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}.
*
* @param javaType
* @param javaType must not be {@literal null}.
*/
public Jackson2JsonRedisSerializer(JavaType javaType) {
this(new ObjectMapper(), javaType);
}

/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}.
*
* @param mapper must not be {@literal null}.
* @param type must not be {@literal null}.
* @since 3.0
*/
public Jackson2JsonRedisSerializer(ObjectMapper mapper, Class<T> type) {

Assert.notNull(mapper, "ObjectMapper must not be null");
Assert.notNull(type, "Java type must not be null");

this.javaType = getJavaType(type);
this.mapper = mapper;
this.reader = JacksonObjectReader.create();
this.writer = JacksonObjectWriter.create();
}

/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}.
*
* @param mapper must not be {@literal null}.
* @param javaType must not be {@literal null}.
* @since 3.0
*/
public Jackson2JsonRedisSerializer(ObjectMapper mapper, JavaType javaType) {
this(mapper, javaType, JacksonObjectReader.create(), JacksonObjectWriter.create());
}

/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}.
*
* @param mapper must not be {@literal null}.
* @param javaType must not be {@literal null}.
* @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
* @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
* @since 3.0
*/
public Jackson2JsonRedisSerializer(ObjectMapper mapper, JavaType javaType, JacksonObjectReader reader,
JacksonObjectWriter writer) {

Assert.notNull(mapper, "ObjectMapper must not be null!");
Assert.notNull(reader, "Reader must not be null!");
Assert.notNull(writer, "Writer must not be null!");

this.mapper = mapper;
this.reader = reader;
this.writer = writer;
this.javaType = javaType;
}

Expand All @@ -70,7 +133,7 @@ public T deserialize(@Nullable byte[] bytes) throws SerializationException {
return null;
}
try {
return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
return (T) this.reader.read(this.mapper, bytes, javaType);
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
Expand All @@ -83,7 +146,7 @@ public byte[] serialize(@Nullable Object t) throws SerializationException {
return SerializationUtils.EMPTY_ARRAY;
}
try {
return this.objectMapper.writeValueAsBytes(t);
return this.writer.write(this.mapper, t);
} catch (Exception ex) {
throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
}
Expand All @@ -97,11 +160,15 @@ public byte[] serialize(@Nullable Object t) throws SerializationException {
* process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for
* specific types. The other option for refining the serialization process is to use Jackson's provided annotations on
* the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
*
* @deprecated since 3.0, use {@link #Jackson2JsonRedisSerializer(ObjectMapper, Class) constructor creation} to
* configure the object mapper.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
@Deprecated(since = "3.0", forRemoval = true)
public void setObjectMapper(ObjectMapper mapper) {

Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
Assert.notNull(mapper, "'objectMapper' must not be null");
this.mapper = mapper;
}

/**
Expand Down
Loading