Skip to content

Commit 361efc7

Browse files
maly7philwebb
authored andcommitted
Support JsonComponent key serializers/deserialzers
Update `@JsonComponent` so that it can also be used to register key serializers and deserializers. See gh-16544
1 parent 063bb90 commit 361efc7

File tree

8 files changed

+411
-24
lines changed

8 files changed

+411
-24
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,17 @@
2424

2525
import com.fasterxml.jackson.databind.JsonDeserializer;
2626
import com.fasterxml.jackson.databind.JsonSerializer;
27+
import com.fasterxml.jackson.databind.KeyDeserializer;
2728

2829
import org.springframework.core.annotation.AliasFor;
2930
import org.springframework.stereotype.Component;
3031

3132
/**
3233
* {@link Component} that provides {@link JsonSerializer} and/or {@link JsonDeserializer}
3334
* implementations to be registered with Jackson when {@link JsonComponentModule} is in
34-
* use. Can be used to annotate {@link JsonSerializer} or {@link JsonDeserializer}
35-
* implementations directly or a class that contains them as inner-classes. For example:
36-
* <pre class="code">
35+
* use. Can be used to annotate {@link JsonSerializer}, {@link JsonDeserializer}, or
36+
* {@link KeyDeserializer} implementations directly or a class that contains them as
37+
* inner-classes. For example: <pre class="code">
3738
* &#064;JsonComponent
3839
* public class CustomerJsonComponent {
3940
*
@@ -71,4 +72,37 @@
7172
@AliasFor(annotation = Component.class)
7273
String value() default "";
7374

75+
/**
76+
* Indicates whether the component should be registered as a type serializer and/or
77+
* deserializer or a key serializer and/or deserializer.
78+
* @return the component's handle type
79+
*/
80+
Handle handle() default Handle.TYPES;
81+
82+
/**
83+
* Specify the classes handled by the serialization and/or deserialization of the
84+
* component. Necessary to be specified for a {@link KeyDeserializer}, as the type
85+
* cannot be inferred. On other types can be used to only handle a subset of
86+
* subclasses.
87+
* @return the classes that should be handled by the component
88+
*/
89+
Class<?>[] handleClasses() default {};
90+
91+
/**
92+
* An enumeration of possible handling types for the component.
93+
*/
94+
enum Handle {
95+
96+
/**
97+
* Register the component as a Type serializer and/or deserializer.
98+
*/
99+
TYPES,
100+
101+
/**
102+
* Register the component as a Key serializer and/or deserializer.
103+
*/
104+
KEYS
105+
106+
}
107+
74108
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import com.fasterxml.jackson.databind.JsonDeserializer;
2525
import com.fasterxml.jackson.databind.JsonSerializer;
26+
import com.fasterxml.jackson.databind.KeyDeserializer;
2627
import com.fasterxml.jackson.databind.Module;
2728
import com.fasterxml.jackson.databind.module.SimpleModule;
2829

@@ -32,12 +33,14 @@
3233
import org.springframework.beans.factory.HierarchicalBeanFactory;
3334
import org.springframework.beans.factory.ListableBeanFactory;
3435
import org.springframework.core.ResolvableType;
36+
import org.springframework.core.annotation.AnnotationUtils;
3537

3638
/**
3739
* Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated
3840
* beans.
3941
*
4042
* @author Phillip Webb
43+
* @author Paul Aly
4144
* @since 1.4.0
4245
* @see JsonComponent
4346
*/
@@ -67,23 +70,32 @@ private void addJsonBeans(ListableBeanFactory beanFactory) {
6770
Map<String, Object> beans = beanFactory
6871
.getBeansWithAnnotation(JsonComponent.class);
6972
for (Object bean : beans.values()) {
70-
addJsonBean(bean);
73+
JsonComponent annotation = AnnotationUtils.findAnnotation(bean.getClass(),
74+
JsonComponent.class);
75+
addJsonBean(bean, annotation);
7176
}
7277
}
7378

74-
private void addJsonBean(Object bean) {
79+
private void addJsonBean(Object bean, JsonComponent annotation) {
7580
if (bean instanceof JsonSerializer) {
76-
addSerializerWithDeducedType((JsonSerializer<?>) bean);
81+
addSerializerForTypes((JsonSerializer<?>) bean, annotation.handle(),
82+
annotation.handleClasses());
83+
}
84+
if (bean instanceof KeyDeserializer) {
85+
addKeyDeserializerForTypes((KeyDeserializer) bean,
86+
annotation.handleClasses());
7787
}
7888
if (bean instanceof JsonDeserializer) {
79-
addDeserializerWithDeducedType((JsonDeserializer<?>) bean);
89+
addDeserializerForTypes((JsonDeserializer<?>) bean,
90+
annotation.handleClasses());
8091
}
8192
for (Class<?> innerClass : bean.getClass().getDeclaredClasses()) {
8293
if (!Modifier.isAbstract(innerClass.getModifiers())
8394
&& (JsonSerializer.class.isAssignableFrom(innerClass)
84-
|| JsonDeserializer.class.isAssignableFrom(innerClass))) {
95+
|| JsonDeserializer.class.isAssignableFrom(innerClass)
96+
|| KeyDeserializer.class.isAssignableFrom(innerClass))) {
8597
try {
86-
addJsonBean(innerClass.newInstance());
98+
addJsonBean(innerClass.newInstance(), annotation);
8799
}
88100
catch (Exception ex) {
89101
throw new IllegalStateException(ex);
@@ -93,17 +105,54 @@ private void addJsonBean(Object bean) {
93105
}
94106

95107
@SuppressWarnings({ "unchecked" })
96-
private <T> void addSerializerWithDeducedType(JsonSerializer<T> serializer) {
97-
ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
98-
serializer.getClass());
99-
addSerializer((Class<T>) type.resolveGeneric(), serializer);
108+
private <T> void addSerializerForTypes(JsonSerializer<T> serializer,
109+
JsonComponent.Handle handle, Class<?>[] types) {
110+
for (Class<?> type : types) {
111+
addSerializerWithType(serializer, handle, (Class<T>) type);
112+
}
113+
114+
if (types.length == 0) {
115+
ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
116+
serializer.getClass());
117+
addSerializerWithType(serializer, handle, (Class<T>) type.resolveGeneric());
118+
}
119+
}
120+
121+
private <T> void addSerializerWithType(JsonSerializer<T> serializer,
122+
JsonComponent.Handle handle, Class<? extends T> type) {
123+
if (JsonComponent.Handle.KEYS.equals(handle)) {
124+
addKeySerializer(type, serializer);
125+
}
126+
else {
127+
addSerializer(type, serializer);
128+
}
129+
}
130+
131+
@SuppressWarnings({ "unchecked" })
132+
private <T> void addDeserializerForTypes(JsonDeserializer<T> deserializer,
133+
Class<?>[] types) {
134+
for (Class<?> type : types) {
135+
addDeserializer((Class<T>) type, deserializer);
136+
}
137+
138+
if (types.length == 0) {
139+
addDeserializerWithDeducedType(deserializer);
140+
}
100141
}
101142

102143
@SuppressWarnings({ "unchecked" })
103144
private <T> void addDeserializerWithDeducedType(JsonDeserializer<T> deserializer) {
104145
ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
105146
deserializer.getClass());
106147
addDeserializer((Class<T>) type.resolveGeneric(), deserializer);
148+
149+
}
150+
151+
private void addKeyDeserializerForTypes(KeyDeserializer deserializer,
152+
Class<?>[] types) {
153+
for (Class<?> type : types) {
154+
addKeyDeserializer(type, deserializer);
155+
}
107156
}
108157

109158
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
package org.springframework.boot.jackson;
1818

19+
import java.io.IOException;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import com.fasterxml.jackson.core.type.TypeReference;
24+
import com.fasterxml.jackson.databind.JsonMappingException;
1925
import com.fasterxml.jackson.databind.Module;
2026
import com.fasterxml.jackson.databind.ObjectMapper;
2127
import org.junit.After;
@@ -24,12 +30,14 @@
2430
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2531

2632
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2734

2835
/**
2936
* Tests for {@link JsonComponentModule}.
3037
*
3138
* @author Phillip Webb
3239
* @author Vladimir Tsanev
40+
* @author Paul Aly
3341
*/
3442
public class JsonComponentModuleTests {
3543

@@ -73,6 +81,38 @@ public void moduleShouldAllowInnerAbstractClasses() throws Exception {
7381
context.close();
7482
}
7583

84+
@Test
85+
public void moduleShouldRegisterKeySerializers() throws Exception {
86+
load(OnlyKeySerializer.class);
87+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
88+
assertKeySerialize(module);
89+
}
90+
91+
@Test
92+
public void moduleShouldRegisterKeyDeserializers() throws Exception {
93+
load(OnlyKeyDeserializer.class);
94+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
95+
assertKeyDeserialize(module);
96+
}
97+
98+
@Test
99+
public void moduleShouldRegisterInnerClassesForKeyHandlers() throws Exception {
100+
load(NameAndAgeJsonKeyComponent.class);
101+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
102+
assertKeySerialize(module);
103+
assertKeyDeserialize(module);
104+
}
105+
106+
@Test
107+
public void moduleShouldRegisterOnlyForSpecifiedClasses() throws Exception {
108+
load(NameAndCareerJsonComponent.class);
109+
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
110+
assertSerialize(module, new NameAndCareer("spring", "developer"),
111+
"{\"name\":\"spring\"}");
112+
assertSerialize(module);
113+
assertDeserializeForSpecifiedClasses(module);
114+
}
115+
76116
private void load(Class<?>... configs) {
77117
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
78118
context.register(configs);
@@ -81,11 +121,17 @@ private void load(Class<?>... configs) {
81121
this.context = context;
82122
}
83123

84-
private void assertSerialize(Module module) throws Exception {
124+
private void assertSerialize(Module module, Name value, String expectedJson)
125+
throws Exception {
85126
ObjectMapper mapper = new ObjectMapper();
86127
mapper.registerModule(module);
87-
String json = mapper.writeValueAsString(new NameAndAge("spring", 100));
88-
assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}");
128+
String json = mapper.writeValueAsString(value);
129+
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
130+
}
131+
132+
private void assertSerialize(Module module) throws Exception {
133+
assertSerialize(module, new NameAndAge("spring", 100),
134+
"{\"name\":\"spring\",\"age\":100}");
89135
}
90136

91137
private void assertDeserialize(Module module) throws Exception {
@@ -97,6 +143,37 @@ private void assertDeserialize(Module module) throws Exception {
97143
assertThat(nameAndAge.getAge()).isEqualTo(100);
98144
}
99145

146+
private void assertDeserializeForSpecifiedClasses(JsonComponentModule module)
147+
throws IOException {
148+
ObjectMapper mapper = new ObjectMapper();
149+
mapper.registerModule(module);
150+
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(() -> mapper
151+
.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class));
152+
NameAndCareer nameAndCareer = mapper.readValue(
153+
"{\"name\":\"spring\",\"career\":\"developer\"}", NameAndCareer.class);
154+
assertThat(nameAndCareer.getName()).isEqualTo("spring");
155+
assertThat(nameAndCareer.getCareer()).isEqualTo("developer");
156+
}
157+
158+
private void assertKeySerialize(Module module) throws Exception {
159+
ObjectMapper mapper = new ObjectMapper();
160+
mapper.registerModule(module);
161+
Map<NameAndAge, Boolean> map = new HashMap<>();
162+
map.put(new NameAndAge("spring", 100), true);
163+
String json = mapper.writeValueAsString(map);
164+
assertThat(json).isEqualToIgnoringWhitespace("{\"spring is 100\": true}");
165+
}
166+
167+
private void assertKeyDeserialize(Module module) throws IOException {
168+
ObjectMapper mapper = new ObjectMapper();
169+
mapper.registerModule(module);
170+
TypeReference<Map<NameAndAge, Boolean>> typeRef = new TypeReference<Map<NameAndAge, Boolean>>() {
171+
};
172+
Map<NameAndAge, Boolean> map = mapper.readValue("{\"spring is 100\": true}",
173+
typeRef);
174+
assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
175+
}
176+
100177
@JsonComponent
101178
static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {
102179

@@ -121,4 +198,14 @@ static class ConcreteSerializer extends AbstractSerializer {
121198

122199
}
123200

201+
@JsonComponent(handle = JsonComponent.Handle.KEYS)
202+
static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
203+
204+
}
205+
206+
@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
207+
static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
208+
209+
}
210+
124211
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2012-2017 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+
17+
package org.springframework.boot.jackson;
18+
19+
/**
20+
* Sample object used for tests.
21+
*
22+
* @author Paul Aly
23+
*/
24+
public class Name {
25+
26+
protected final String name;
27+
28+
public Name(String name) {
29+
this.name = name;
30+
}
31+
32+
public String getName() {
33+
return this.name;
34+
}
35+
36+
}

0 commit comments

Comments
 (0)