1616package org .springframework .data .redis .serializer ;
1717
1818import java .io .IOException ;
19+ import java .io .Serial ;
1920import java .util .Collections ;
2021import java .util .function .Supplier ;
2122
2223import org .springframework .cache .support .NullValue ;
2324import org .springframework .core .KotlinDetector ;
25+ import org .springframework .data .redis .util .RedisAssertions ;
2426import org .springframework .data .util .Lazy ;
2527import org .springframework .lang .Nullable ;
2628import org .springframework .util .Assert ;
4648import com .fasterxml .jackson .databind .type .TypeFactory ;
4749
4850/**
49- * Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing.
51+ * Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to and from {@literal JSON}
52+ * using dynamic typing.
5053 * <p>
51- * JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective
52- * {@link JacksonObjectWriter}.
54+ * {@literal JSON} reading and writing can be customized by configuring a {@link JacksonObjectReader}
55+ * and {@link JacksonObjectWriter}.
5356 *
5457 * @author Christoph Strobl
5558 * @author Mark Paluch
5659 * @author Mao Shuai
60+ * @author John Blum
61+ * @see org.springframework.data.redis.serializer.JacksonObjectReader
62+ * @see org.springframework.data.redis.serializer.JacksonObjectWriter
63+ * @see com.fasterxml.jackson.databind.ObjectMapper
5764 * @since 1.6
5865 */
5966public class GenericJackson2JsonRedisSerializer implements RedisSerializer <Object > {
6067
61- private final ObjectMapper mapper ;
68+ /**
69+ * Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
70+ * {@code classPropertyTypeName}. This method should be called by code that customizes
71+ * {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
72+ *
73+ * @param objectMapper the object mapper to customize.
74+ * @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
75+ * @since 2.2
76+ */
77+ public static void registerNullValueSerializer (ObjectMapper objectMapper , @ Nullable String classPropertyTypeName ) {
78+
79+ // Simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here
80+ // since we need the type hint embedded for deserialization using the default typing feature.
81+ objectMapper .registerModule (new SimpleModule ().addSerializer (new NullValueSerializer (classPropertyTypeName )));
82+ }
6283
6384 private final JacksonObjectReader reader ;
6485
6586 private final JacksonObjectWriter writer ;
6687
6788 private final Lazy <Boolean > defaultTypingEnabled ;
6889
90+ private final ObjectMapper mapper ;
91+
6992 private final TypeResolver typeResolver ;
7093
7194 /**
72- * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
95+ * Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
96+ * default typing.
7397 */
7498 public GenericJackson2JsonRedisSerializer () {
7599 this ((String ) null );
76100 }
77101
78102 /**
79- * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the
80- * given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
81- * {@link JsonTypeInfo.Id#CLASS} will be used.
103+ * Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
104+ * default typing using the given {@link String name}.
105+ * <p>
106+ * In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS}
107+ * will be used.
82108 *
83- * @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
109+ * @param classPropertyTypeName {@link String name} of the JSON property holding type information;
110+ * can be {@literal null}.
84111 * @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
85112 * @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
86113 */
@@ -89,13 +116,17 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
89116 }
90117
91118 /**
92- * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the
93- * given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
94- * {@link JsonTypeInfo.Id#CLASS} will be used.
119+ * Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
120+ * default typing using the given {@link String name} along with the given, required {@link JacksonObjectReader}
121+ * and {@link JacksonObjectWriter} used to read/write {@link Object Objects} de/serialized as JSON.
122+ * <p>
123+ * In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS}
124+ * will be used.
95125 *
96- * @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
97- * @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
98- * @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
126+ * @param classPropertyTypeName {@link String name} of the JSON property holding type information;
127+ * can be {@literal null}.
128+ * @param reader {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
129+ * @param writer {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
99130 * @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
100131 * @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
101132 * @since 3.0
@@ -105,19 +136,17 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
105136
106137 this (new ObjectMapper (), reader , writer , classPropertyTypeName );
107138
108- // simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
109- // the type hint embedded for deserialization using the default typing feature.
110- registerNullValueSerializer (mapper , classPropertyTypeName );
139+ registerNullValueSerializer (this .mapper , classPropertyTypeName );
111140
112- StdTypeResolverBuilder typer = new TypeResolverBuilder (DefaultTyping .EVERYTHING ,
113- mapper .getPolymorphicTypeValidator ());
114- typer = typer .init (JsonTypeInfo .Id .CLASS , null );
115- typer = typer .inclusion (JsonTypeInfo .As .PROPERTY );
141+ StdTypeResolverBuilder typer = TypeResolverBuilder .forEverything (this .mapper )
142+ .init (JsonTypeInfo .Id .CLASS , null )
143+ .inclusion (JsonTypeInfo .As .PROPERTY );
116144
117145 if (StringUtils .hasText (classPropertyTypeName )) {
118146 typer = typer .typeProperty (classPropertyTypeName );
119147 }
120- mapper .setDefaultTyping (typer );
148+
149+ this .mapper .setDefaultTyping (typer );
121150 }
122151
123152 /**
@@ -143,58 +172,34 @@ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
143172 */
144173 public GenericJackson2JsonRedisSerializer (ObjectMapper mapper , JacksonObjectReader reader ,
145174 JacksonObjectWriter writer ) {
175+
146176 this (mapper , reader , writer , null );
147177 }
148178
149179 private GenericJackson2JsonRedisSerializer (ObjectMapper mapper , JacksonObjectReader reader ,
150180 JacksonObjectWriter writer , @ Nullable String typeHintPropertyName ) {
151181
152- Assert .notNull (mapper , "ObjectMapper must not be null" );
153- Assert .notNull (reader , "Reader must not be null" );
154- Assert .notNull (writer , "Writer must not be null" );
155-
156- this .mapper = mapper ;
157- this .reader = reader ;
158- this .writer = writer ;
182+ this .mapper = RedisAssertions .requireObject (mapper , "ObjectMapper must not be null" );
183+ this .reader = RedisAssertions .requireObject (reader , "Reader must not be null" );
184+ this .writer = RedisAssertions .requireObject (writer , "Writer must not be null" );
159185
160- this .defaultTypingEnabled = Lazy .of (() -> mapper .getSerializationConfig ().getDefaultTyper (null ) != null );
186+ this .defaultTypingEnabled = Lazy .of (() -> mapper .getSerializationConfig ()
187+ .getDefaultTyper (null ) != null );
161188
162- Supplier < String > typeHintPropertyNameSupplier ;
163-
164- if ( typeHintPropertyName == null ) {
189+ this . typeResolver = new TypeResolver ( Lazy . of ( mapper :: getTypeFactory ),
190+ newTypeHintPropertyNameSupplier ( mapper , typeHintPropertyName , this . defaultTypingEnabled ));
191+ }
165192
166- typeHintPropertyNameSupplier = Lazy .of (() -> {
167- if (defaultTypingEnabled .get ()) {
168- return null ;
169- }
193+ private Supplier <String > newTypeHintPropertyNameSupplier (ObjectMapper mapper , @ Nullable String typeHintPropertyName ,
194+ Lazy <Boolean > defaultTypingEnabled ) {
170195
171- return mapper .getDeserializationConfig ().getDefaultTyper (null )
196+ return typeHintPropertyName != null ? () -> typeHintPropertyName
197+ : Lazy .of (() -> defaultTypingEnabled .get () ? null
198+ : mapper .getDeserializationConfig ().getDefaultTyper (null )
172199 .buildTypeDeserializer (mapper .getDeserializationConfig (),
173200 mapper .getTypeFactory ().constructType (Object .class ), Collections .emptyList ())
174- .getPropertyName ();
175-
176- }).or ("@class" );
177- } else {
178- typeHintPropertyNameSupplier = () -> typeHintPropertyName ;
179- }
180-
181- this .typeResolver = new TypeResolver (Lazy .of (mapper ::getTypeFactory ), typeHintPropertyNameSupplier );
182- }
183-
184- /**
185- * Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
186- * {@code classPropertyTypeName}. This method should be called by code that customizes
187- * {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
188- *
189- * @param objectMapper the object mapper to customize.
190- * @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
191- * @since 2.2
192- */
193- public static void registerNullValueSerializer (ObjectMapper objectMapper , @ Nullable String classPropertyTypeName ) {
194-
195- // simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
196- // the type hint embedded for deserialization using the default typing feature.
197- objectMapper .registerModule (new SimpleModule ().addSerializer (new NullValueSerializer (classPropertyTypeName )));
201+ .getPropertyName ())
202+ .or ("@class" );
198203 }
199204
200205 @ Override
@@ -206,8 +211,9 @@ public byte[] serialize(@Nullable Object source) throws SerializationException {
206211
207212 try {
208213 return writer .write (mapper , source );
209- } catch (IOException e ) {
210- throw new SerializationException ("Could not write JSON: " + e .getMessage (), e );
214+ } catch (IOException cause ) {
215+ String message = String .format ("Could not write JSON: %s" , cause .getMessage ());
216+ throw new SerializationException (message , cause );
211217 }
212218 }
213219
@@ -217,17 +223,24 @@ public Object deserialize(@Nullable byte[] source) throws SerializationException
217223 }
218224
219225 /**
220- * @param source can be {@literal null}.
221- * @param type must not be {@literal null}.
222- * @return {@literal null} for empty source.
223- * @throws SerializationException
226+ * Deserialized the array of bytes containing {@literal JSON} as an {@link Object} of the given,
227+ * required {@link Class type}.
228+ *
229+ * @param source array of bytes containing the {@literal JSON} to deserialize; can be {@literal null}.
230+ * @param type {@link Class type} of {@link Object} from which the {@literal JSON} will be deserialized;
231+ * must not be {@literal null}.
232+ * @return {@literal null} for an empty source, or an {@link Object} of the given {@link Class type}
233+ * deserialized from the array of bytes containing {@literal JSON}.
234+ * @throws IllegalArgumentException if the given {@link Class type} is {@literal null}.
235+ * @throws SerializationException if the array of bytes cannot be deserialized as an instance of
236+ * the given {@link Class type}
224237 */
225238 @ Nullable
226239 @ SuppressWarnings ("unchecked" )
227240 public <T > T deserialize (@ Nullable byte [] source , Class <T > type ) throws SerializationException {
228241
229- Assert .notNull (type ,
230- "Deserialization type must not be null Please provide Object.class to make use of Jackson2 default typing." );
242+ Assert .notNull (type , "Deserialization type must not be null;"
243+ + " Please provide Object.class to make use of Jackson2 default typing." );
231244
232245 if (SerializationUtils .isEmpty (source )) {
233246 return null ;
@@ -292,7 +305,9 @@ protected JavaType resolveType(byte[] source, Class<?> type) throws IOException
292305 */
293306 private static class NullValueSerializer extends StdSerializer <NullValue > {
294307
308+ @ Serial
295309 private static final long serialVersionUID = 1999052150548658808L ;
310+
296311 private final String classIdentifier ;
297312
298313 /**
@@ -305,17 +320,19 @@ private static class NullValueSerializer extends StdSerializer<NullValue> {
305320 }
306321
307322 @ Override
308- public void serialize (NullValue value , JsonGenerator jgen , SerializerProvider provider ) throws IOException {
323+ public void serialize (NullValue value , JsonGenerator jsonGenerator , SerializerProvider provider )
324+ throws IOException {
309325
310- jgen .writeStartObject ();
311- jgen .writeStringField (classIdentifier , NullValue .class .getName ());
312- jgen .writeEndObject ();
326+ jsonGenerator .writeStartObject ();
327+ jsonGenerator .writeStringField (classIdentifier , NullValue .class .getName ());
328+ jsonGenerator .writeEndObject ();
313329 }
314330
315331 @ Override
316- public void serializeWithType (NullValue value , JsonGenerator gen , SerializerProvider serializers ,
317- TypeSerializer typeSer ) throws IOException {
318- serialize (value , gen , serializers );
332+ public void serializeWithType (NullValue value , JsonGenerator jsonGenerator , SerializerProvider serializers ,
333+ TypeSerializer typeSerializer ) throws IOException {
334+
335+ serialize (value , jsonGenerator , serializers );
319336 }
320337 }
321338
@@ -329,8 +346,12 @@ public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProv
329346 */
330347 private static class TypeResolverBuilder extends ObjectMapper .DefaultTypeResolverBuilder {
331348
332- public TypeResolverBuilder (DefaultTyping t , PolymorphicTypeValidator ptv ) {
333- super (t , ptv );
349+ static TypeResolverBuilder forEverything (ObjectMapper mapper ) {
350+ return new TypeResolverBuilder (DefaultTyping .EVERYTHING , mapper .getPolymorphicTypeValidator ());
351+ }
352+
353+ public TypeResolverBuilder (DefaultTyping typing , PolymorphicTypeValidator polymorphicTypeValidator ) {
354+ super (typing , polymorphicTypeValidator );
334355 }
335356
336357 @ Override
@@ -343,25 +364,25 @@ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class<?> defaultI
343364 * Boolean, Integer, Double) will never use typing; that is both due to them being concrete and final, and since
344365 * actual serializers and deserializers will also ignore any attempts to enforce typing.
345366 */
346- public boolean useForType (JavaType t ) {
367+ public boolean useForType (JavaType javaType ) {
347368
348- if (t .isJavaLangObject ()) {
369+ if (javaType .isJavaLangObject ()) {
349370 return true ;
350371 }
351372
352- t = resolveArrayOrWrapper (t );
373+ javaType = resolveArrayOrWrapper (javaType );
353374
354- if (t .isEnumType () || ClassUtils .isPrimitiveOrWrapper (t .getRawClass ())) {
375+ if (javaType .isEnumType () || ClassUtils .isPrimitiveOrWrapper (javaType .getRawClass ())) {
355376 return false ;
356377 }
357378
358- if (t .isFinal () && !KotlinDetector .isKotlinType (t .getRawClass ())
359- && t .getRawClass ().getPackageName ().startsWith ("java" )) {
379+ if (javaType .isFinal () && !KotlinDetector .isKotlinType (javaType .getRawClass ())
380+ && javaType .getRawClass ().getPackageName ().startsWith ("java" )) {
360381 return false ;
361382 }
362383
363384 // [databind#88] Should not apply to JSON tree models:
364- return !TreeNode .class .isAssignableFrom (t .getRawClass ());
385+ return !TreeNode .class .isAssignableFrom (javaType .getRawClass ());
365386 }
366387
367388 private JavaType resolveArrayOrWrapper (JavaType type ) {
0 commit comments