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

We now encapsulate serialization and deserialization operations as JacksonObjectWriter and JacksonObjectReader functions to allow customization of Jackson serialization.

Closes: #2322
Original Pull Request: #2332
This commit is contained in:
Mark Paluch
2022-05-24 14:28:19 +02:00
committed by Christoph Strobl
parent 862decfdbe
commit eb7dfbc7ef
6 changed files with 239 additions and 11 deletions

View File

@@ -25,7 +25,6 @@ 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.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializerProvider;
@@ -37,6 +36,9 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* 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
@@ -47,6 +49,10 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
private final ObjectMapper mapper;
private final JacksonObjectReader reader;
private final JacksonObjectWriter writer;
/**
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
*/
@@ -59,13 +65,30 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
* 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());
}
this(new ObjectMapper());
/**
* 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(), reader, writer);
// 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.
@@ -87,9 +110,29 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
* @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) {
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;
}
/**
@@ -116,8 +159,8 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
}
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);
}
}
@@ -134,6 +177,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
* @throws SerializationException
*/
@Nullable
@SuppressWarnings("unchecked")
public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {
Assert.notNull(type,
@@ -144,7 +188,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
}
try {
return mapper.readValue(source, type);
return (T) reader.read(mapper, source, mapper.getTypeFactory().constructType(type));
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
@@ -172,8 +216,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
}
@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());
@@ -186,4 +229,5 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
serialize(value, gen, serializers);
}
}
}

View File

@@ -31,10 +31,14 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
* <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> {
@@ -45,6 +49,10 @@ public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
private JacksonObjectReader reader = JacksonObjectReader.create();
private JacksonObjectWriter writer = JacksonObjectWriter.create();
/**
* Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}.
*
@@ -70,7 +78,7 @@ public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
return null;
}
try {
return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType);
return (T) this.reader.read(this.objectMapper, bytes, javaType);
} catch (Exception ex) {
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
@@ -83,7 +91,7 @@ public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
return SerializationUtils.EMPTY_ARRAY;
}
try {
return this.objectMapper.writeValueAsBytes(t);
return this.writer.write(this.objectMapper, t);
} catch (Exception ex) {
throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
}
@@ -104,6 +112,26 @@ public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
this.objectMapper = objectMapper;
}
/**
* Sets the {@link JacksonObjectReader} for this serializer. Setting the reader allows customization of the JSON
* deserialization.
*
* @since 3.0
*/
public void setReader(JacksonObjectReader reader) {
this.reader = reader;
}
/**
* Sets the {@link JacksonObjectWriter} for this serializer. Setting the reader allows customization of the JSON
* serialization.
*
* @since 3.0
*/
public void setWriter(JacksonObjectWriter writer) {
this.writer = writer;
}
/**
* Returns the Jackson {@link JavaType} for the specific class.
* <p>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2022 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.io.IOException;
import java.io.InputStream;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Defines the contract for Object Mapping readers. Implementations of this interface can deserialize a given byte array
* holding JSON to an Object considering the target type.
* <p>
* Reader functions can customize how the actual JSON is being deserialized by e.g. obtaining a customized
* {@link com.fasterxml.jackson.databind.ObjectReader} applying serialization features, date formats, or views.
*
* @author Mark Paluch
* @since 3.0
*/
@FunctionalInterface
public interface JacksonObjectReader {
/**
* Read an object graph from the given root JSON into a Java object considering the {@link JavaType}.
*
* @param mapper the object mapper to use.
* @param source the JSON to deserialize.
* @param type the Java target type
* @return the deserialized Java object.
* @throws IOException if an I/O error or JSON deserialization error occurs.
*/
Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException;
/**
* Create a default {@link JacksonObjectReader} delegating to {@link ObjectMapper#readValue(InputStream, JavaType)}.
*
* @return the default {@link JacksonObjectReader}.
*/
static JacksonObjectReader create() {
return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type);
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2022 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.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Defines the contract for Object Mapping writers. Implementations of this interface can serialize a given Object to a
* {@code byte[]} containing JSON.
* <p>
* Writer functions can customize how the actual JSON is being written by e.g. obtaining a customized
* {@link com.fasterxml.jackson.databind.ObjectWriter} applying serialization features, date formats, or views.
*
* @author Mark Paluch
* @since 3.0
*/
@FunctionalInterface
public interface JacksonObjectWriter {
/**
* Write the object graph with the given root {@code source} as byte array.
*
* @param mapper the object mapper to use.
* @param source the root of the object graph to marshal.
* @return a byte array containing the serialized object graph.
* @throws IOException if an I/O error or JSON serialization error occurs.
*/
byte[] write(ObjectMapper mapper, Object source) throws IOException;
/**
* Create a default {@link JacksonObjectWriter} delegating to {@link ObjectMapper#writeValueAsBytes(Object)}.
*
* @return the default {@link JacksonObjectWriter}.
*/
static JacksonObjectWriter create() {
return ObjectMapper::writeValueAsBytes;
}
}