Skip to content

Commit 58eb31c

Browse files
Hacking: Jackson Serializers
1 parent ae73f7e commit 58eb31c

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.serializer;
17+
18+
import tools.jackson.databind.JavaType;
19+
import tools.jackson.databind.ObjectMapper;
20+
import tools.jackson.databind.ser.SerializerFactory;
21+
import tools.jackson.databind.type.TypeFactory;
22+
23+
import java.nio.charset.Charset;
24+
import java.nio.charset.StandardCharsets;
25+
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* {@link RedisSerializer} that can read and write JSON using
31+
* <a href="https://github.com/FasterXML/jackson-core">Jackson's</a> and
32+
* <a href="https://github.com/FasterXML/jackson-databind">Jackson Databind</a> {@link ObjectMapper}.
33+
* <p>
34+
* This serializer can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances.
35+
* <b>Note:</b>Null objects are serialized as empty arrays and vice versa.
36+
* <p>
37+
* JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective
38+
* {@link JacksonObjectWriter}.
39+
*
40+
* @author Thomas Darimont
41+
* @author Mark Paluch
42+
* @since 1.2
43+
*/
44+
public class Jackson3JsonRedisSerializer<T> implements RedisSerializer<T> {
45+
46+
/**
47+
* @deprecated since 3.0 for removal.
48+
*/
49+
@Deprecated(since = "3.0", forRemoval = true) //
50+
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
51+
52+
private final JavaType javaType;
53+
54+
private ObjectMapper mapper;
55+
56+
private final Jackson3ObjectReader reader;
57+
58+
private final Jackson3ObjectWriter writer;
59+
60+
/**
61+
* Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link Class}.
62+
*
63+
* @param type must not be {@literal null}.
64+
*/
65+
public Jackson3JsonRedisSerializer(Class<T> type) {
66+
this(new ObjectMapper(), type);
67+
}
68+
69+
/**
70+
* Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}.
71+
*
72+
* @param javaType must not be {@literal null}.
73+
*/
74+
public Jackson3JsonRedisSerializer(JavaType javaType) {
75+
this(new ObjectMapper(), javaType);
76+
}
77+
78+
/**
79+
* Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link Class}.
80+
*
81+
* @param mapper must not be {@literal null}.
82+
* @param type must not be {@literal null}.
83+
* @since 3.0
84+
*/
85+
public Jackson3JsonRedisSerializer(ObjectMapper mapper, Class<T> type) {
86+
87+
Assert.notNull(mapper, "ObjectMapper must not be null");
88+
Assert.notNull(type, "Java type must not be null");
89+
90+
this.javaType = getJavaType(type);
91+
this.mapper = mapper;
92+
this.reader = Jackson3ObjectReader.create();
93+
this.writer = Jackson3ObjectWriter.create();
94+
}
95+
96+
/**
97+
* Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}.
98+
*
99+
* @param mapper must not be {@literal null}.
100+
* @param javaType must not be {@literal null}.
101+
* @since 3.0
102+
*/
103+
public Jackson3JsonRedisSerializer(ObjectMapper mapper, JavaType javaType) {
104+
this(mapper, javaType, Jackson3ObjectReader.create(), Jackson3ObjectWriter.create());
105+
}
106+
107+
/**
108+
* Creates a new {@link Jackson3JsonRedisSerializer} for the given target {@link JavaType}.
109+
*
110+
* @param mapper must not be {@literal null}.
111+
* @param javaType must not be {@literal null}.
112+
* @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
113+
* @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
114+
* @since 3.0
115+
*/
116+
public Jackson3JsonRedisSerializer(ObjectMapper mapper, JavaType javaType, Jackson3ObjectReader reader,
117+
Jackson3ObjectWriter writer) {
118+
119+
Assert.notNull(mapper, "ObjectMapper must not be null!");
120+
Assert.notNull(reader, "Reader must not be null!");
121+
Assert.notNull(writer, "Writer must not be null!");
122+
123+
this.mapper = mapper;
124+
this.reader = reader;
125+
this.writer = writer;
126+
this.javaType = javaType;
127+
}
128+
129+
/**
130+
* Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper}
131+
* is used.
132+
* <p>
133+
* Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON serialization
134+
* process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for
135+
* specific types. The other option for refining the serialization process is to use Jackson's provided annotations on
136+
* the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
137+
*
138+
* @deprecated since 3.0, use {@link #Jackson3JsonRedisSerializer(ObjectMapper, Class) constructor creation} to
139+
* configure the object mapper.
140+
*/
141+
@Deprecated(since = "3.0", forRemoval = true)
142+
public void setObjectMapper(ObjectMapper mapper) {
143+
144+
Assert.notNull(mapper, "'objectMapper' must not be null");
145+
this.mapper = mapper;
146+
}
147+
148+
@Override
149+
public byte[] serialize(@Nullable T value) throws SerializationException {
150+
151+
if (value == null) {
152+
return SerializationUtils.EMPTY_ARRAY;
153+
}
154+
try {
155+
return this.writer.write(this.mapper, value);
156+
} catch (Exception ex) {
157+
throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex);
158+
}
159+
}
160+
161+
@Nullable
162+
@Override
163+
@SuppressWarnings("unchecked")
164+
public T deserialize(@Nullable byte[] bytes) throws SerializationException {
165+
166+
if (SerializationUtils.isEmpty(bytes)) {
167+
return null;
168+
}
169+
try {
170+
return (T) this.reader.read(this.mapper, bytes, javaType);
171+
} catch (Exception ex) {
172+
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
173+
}
174+
}
175+
176+
/**
177+
* Returns the Jackson {@link JavaType} for the specific class.
178+
* <p>
179+
* Default implementation returns {@link TypeFactory#constructType(java.lang.reflect.Type)}, but this can be
180+
* overridden in subclasses, to allow for custom generic collection handling. For instance:
181+
*
182+
* <pre class="code">
183+
* protected JavaType getJavaType(Class&lt;?&gt; clazz) {
184+
* if (List.class.isAssignableFrom(clazz)) {
185+
* return TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, MyBean.class);
186+
* } else {
187+
* return super.getJavaType(clazz);
188+
* }
189+
* }
190+
* </pre>
191+
*
192+
* @param clazz the class to return the java type for
193+
* @return the java type
194+
*/
195+
protected JavaType getJavaType(Class<?> clazz) {
196+
return TypeFactory.unsafeSimpleType(clazz);
197+
}
198+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2022-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.serializer;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
21+
import tools.jackson.databind.JavaType;
22+
import tools.jackson.databind.ObjectMapper;
23+
24+
/**
25+
* Defines the contract for Object Mapping readers. Implementations of this interface can deserialize a given byte array
26+
* holding JSON to an Object considering the target type.
27+
* <p>
28+
* Reader functions can customize how the actual JSON is being deserialized by e.g. obtaining a customized
29+
* {@link com.fasterxml.jackson.databind.ObjectReader} applying serialization features, date formats, or views.
30+
*
31+
* @author Mark Paluch
32+
* @since 3.0
33+
*/
34+
@FunctionalInterface
35+
public interface Jackson3ObjectReader {
36+
37+
/**
38+
* Read an object graph from the given root JSON into a Java object considering the {@link JavaType}.
39+
*
40+
* @param mapper the object mapper to use.
41+
* @param source the JSON to deserialize.
42+
* @param type the Java target type
43+
* @return the deserialized Java object.
44+
* @throws IOException if an I/O error or JSON deserialization error occurs.
45+
*/
46+
Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException;
47+
48+
/**
49+
* Create a default {@link Jackson3ObjectReader} delegating to {@link ObjectMapper#readValue(InputStream, JavaType)}.
50+
*
51+
* @return the default {@link Jackson3ObjectReader}.
52+
*/
53+
static Jackson3ObjectReader create() {
54+
return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type);
55+
}
56+
57+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2022-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.serializer;
17+
18+
import java.io.IOException;
19+
20+
import tools.jackson.databind.ObjectMapper;
21+
22+
/**
23+
* Defines the contract for Object Mapping writers. Implementations of this interface can serialize a given Object to a
24+
* {@code byte[]} containing JSON.
25+
* <p>
26+
* Writer functions can customize how the actual JSON is being written by e.g. obtaining a customized
27+
* {@link com.fasterxml.jackson.databind.ObjectWriter} applying serialization features, date formats, or views.
28+
*
29+
* @author Mark Paluch
30+
* @since 3.0
31+
*/
32+
@FunctionalInterface
33+
public interface Jackson3ObjectWriter {
34+
35+
/**
36+
* Write the object graph with the given root {@code source} as byte array.
37+
*
38+
* @param mapper the object mapper to use.
39+
* @param source the root of the object graph to marshal.
40+
* @return a byte array containing the serialized object graph.
41+
* @throws IOException if an I/O error or JSON serialization error occurs.
42+
*/
43+
byte[] write(ObjectMapper mapper, Object source) throws IOException;
44+
45+
/**
46+
* Create a default {@link Jackson3ObjectWriter} delegating to {@link ObjectMapper#writeValueAsBytes(Object)}.
47+
*
48+
* @return the default {@link Jackson3ObjectWriter}.
49+
*/
50+
static Jackson3ObjectWriter create() {
51+
return ObjectMapper::writeValueAsBytes;
52+
}
53+
54+
}

src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
3535
import org.springframework.data.redis.serializer.GenericToStringSerializer;
3636
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
37+
import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer;
3738
import org.springframework.data.redis.serializer.OxmSerializer;
3839
import org.springframework.data.redis.serializer.StringRedisSerializer;
3940
import org.springframework.data.redis.test.XstreamOxmSerializerSingleton;
@@ -109,6 +110,12 @@ public static Collection<Object[]> testParams(RedisConnectionFactory connectionF
109110
jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer);
110111
jackson2JsonPersonTemplate.afterPropertiesSet();
111112

113+
Jackson3JsonRedisSerializer<Person> jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class);
114+
RedisTemplate<String, Person> jackson3JsonPersonTemplate = new RedisTemplate<>();
115+
jackson3JsonPersonTemplate.setConnectionFactory(connectionFactory);
116+
jackson3JsonPersonTemplate.setValueSerializer(jackson3JsonSerializer);
117+
jackson3JsonPersonTemplate.afterPropertiesSet();
118+
112119
GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer();
113120
RedisTemplate<String, Person> genericJackson2JsonPersonTemplate = new RedisTemplate<>();
114121
genericJackson2JsonPersonTemplate.setConnectionFactory(connectionFactory);
@@ -124,6 +131,7 @@ public static Collection<Object[]> testParams(RedisConnectionFactory connectionF
124131
{ xstreamStringTemplate, stringFactory, stringFactory }, //
125132
{ xstreamPersonTemplate, stringFactory, personFactory }, //
126133
{ jackson2JsonPersonTemplate, stringFactory, personFactory }, //
134+
{ jackson3JsonPersonTemplate, stringFactory, personFactory }, //
127135
{ genericJackson2JsonPersonTemplate, stringFactory, personFactory } });
128136
}
129137
}

src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
3535
import org.springframework.data.redis.serializer.GenericToStringSerializer;
3636
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
37+
import org.springframework.data.redis.serializer.Jackson3JsonRedisSerializer;
3738
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
3839
import org.springframework.data.redis.serializer.OxmSerializer;
3940
import org.springframework.data.redis.serializer.RedisSerializationContext;
@@ -100,6 +101,10 @@ RedisSerializationContext.<String, Double> newSerializationContext(jdkSerializat
100101
ReactiveRedisTemplate<String, Person> jackson2JsonPersonTemplate = new ReactiveRedisTemplate(
101102
lettuceConnectionFactory, RedisSerializationContext.fromSerializer(jackson2JsonSerializer));
102103

104+
Jackson3JsonRedisSerializer<Person> jackson3JsonSerializer = new Jackson3JsonRedisSerializer<>(Person.class);
105+
ReactiveRedisTemplate<String, Person> jackson3JsonPersonTemplate = new ReactiveRedisTemplate(
106+
lettuceConnectionFactory, RedisSerializationContext.fromSerializer(jackson3JsonSerializer));
107+
103108
GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer();
104109
ReactiveRedisTemplate<String, Person> genericJackson2JsonPersonTemplate = new ReactiveRedisTemplate(
105110
lettuceConnectionFactory, RedisSerializationContext.fromSerializer(genericJackson2JsonSerializer));
@@ -115,6 +120,7 @@ RedisSerializationContext.<String, Double> newSerializationContext(jdkSerializat
115120
new Fixture<>(xstreamStringTemplate, stringFactory, stringFactory, oxmSerializer, "String/OXM"), //
116121
new Fixture<>(xstreamPersonTemplate, stringFactory, personFactory, oxmSerializer, "String/Person/OXM"), //
117122
new Fixture<>(jackson2JsonPersonTemplate, stringFactory, personFactory, jackson2JsonSerializer, "Jackson2"), //
123+
new Fixture<>(jackson3JsonPersonTemplate, stringFactory, personFactory, jackson2JsonSerializer, "Jackson3"), //
118124
new Fixture<>(genericJackson2JsonPersonTemplate, stringFactory, personFactory, genericJackson2JsonSerializer,
119125
"Generic Jackson 2"));
120126

0 commit comments

Comments
 (0)