Skip to content

Add annotation to support registering Jackson key serializer/deserializers #16544

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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
939ac24
add handle attribute to JsonComponent
maly7 Apr 9, 2019
c7a0ef7
add two tests for key serialization and deserialization
maly7 Apr 10, 2019
062cca5
registering key serializers works
maly7 Apr 10, 2019
c83c23c
add equals and hashCode for NameAndAge
maly7 Apr 10, 2019
653be19
apply the formatter
maly7 Apr 10, 2019
997fb8c
use StdKeyDeserializer for registering key deserializers
maly7 Apr 10, 2019
e4ebb4c
register inner StdKeyDeserializer classes
maly7 Apr 10, 2019
fccdbd6
specify the key type in the annotation
maly7 Apr 10, 2019
b73df97
adjust method and parameter names to be more intentional
maly7 Apr 10, 2019
e9fbeb8
only register serializers for specified classes
maly7 Apr 12, 2019
4ff0d6a
only register deserializers for specified classes
maly7 Apr 12, 2019
5c62b76
module should respect inner class annotations
maly7 Apr 12, 2019
bf0e39d
remove unintended support for inner annotations
maly7 Apr 12, 2019
f6226b2
update JsonComponent documentation
maly7 Apr 12, 2019
df773b1
update documentation
maly7 Apr 12, 2019
e2f9b64
fix formatting
maly7 Apr 12, 2019
de931ba
add handle attribute to JsonComponent
maly7 Apr 9, 2019
a56e1db
add two tests for key serialization and deserialization
maly7 Apr 10, 2019
1d66107
registering key serializers works
maly7 Apr 10, 2019
521799d
add equals and hashCode for NameAndAge
maly7 Apr 10, 2019
b5b8930
apply the formatter
maly7 Apr 10, 2019
9ebeafa
use StdKeyDeserializer for registering key deserializers
maly7 Apr 10, 2019
4a9bb5e
register inner StdKeyDeserializer classes
maly7 Apr 10, 2019
19024a3
specify the key type in the annotation
maly7 Apr 10, 2019
3a0a9d2
adjust method and parameter names to be more intentional
maly7 Apr 10, 2019
e611fc7
only register serializers for specified classes
maly7 Apr 12, 2019
03e333f
only register deserializers for specified classes
maly7 Apr 12, 2019
53032aa
module should respect inner class annotations
maly7 Apr 12, 2019
835bcaa
remove unintended support for inner annotations
maly7 Apr 12, 2019
dc33d33
update JsonComponent documentation
maly7 Apr 12, 2019
97f32c2
update documentation
maly7 Apr 12, 2019
8a8af75
fix formatting
maly7 Apr 12, 2019
643b3fa
Merge branch 'support-jackson-key-serializers' of github.com:maly7/sp…
maly7 Apr 12, 2019
e2af92e
add ASF license header
maly7 Apr 12, 2019
3b7c8d7
fix checkstyle violations
maly7 Apr 12, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

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

/**
* Indicates whether the component should be registered as a type serializer and/or
* deserializer or a key serializer and/or deserializer.
* @return the component's handle type
*/
Handle handle() default Handle.TYPES;

/**
* Specify the classes handled by the serialization and/or deserialization of the
* component. Necessary to be specified for a {@link KeyDeserializer}, as the type
* cannot be inferred. On other types can be used to only handle a subset of
* subclasses.
* @return the classes that should be handled by the component
*/
Class<?>[] handleClasses() default {};

/**
* An enumeration of possible handling types for the component.
*/
enum Handle {

/**
* Register the component as a Type serializer and/or deserializer.
*/
TYPES,

/**
* Register the component as a Key serializer and/or deserializer.
*/
KEYS

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;

Expand All @@ -32,12 +33,14 @@
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;

/**
* Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated
* beans.
*
* @author Phillip Webb
* @author Paul Aly
* @since 1.4.0
* @see JsonComponent
*/
Expand Down Expand Up @@ -67,23 +70,32 @@ private void addJsonBeans(ListableBeanFactory beanFactory) {
Map<String, Object> beans = beanFactory
.getBeansWithAnnotation(JsonComponent.class);
for (Object bean : beans.values()) {
addJsonBean(bean);
JsonComponent annotation = AnnotationUtils.findAnnotation(bean.getClass(),
JsonComponent.class);
addJsonBean(bean, annotation);
}
}

private void addJsonBean(Object bean) {
private void addJsonBean(Object bean, JsonComponent annotation) {
if (bean instanceof JsonSerializer) {
addSerializerWithDeducedType((JsonSerializer<?>) bean);
addSerializerForTypes((JsonSerializer<?>) bean, annotation.handle(),
annotation.handleClasses());
}
if (bean instanceof KeyDeserializer) {
addKeyDeserializerForTypes((KeyDeserializer) bean,
annotation.handleClasses());
}
if (bean instanceof JsonDeserializer) {
addDeserializerWithDeducedType((JsonDeserializer<?>) bean);
addDeserializerForTypes((JsonDeserializer<?>) bean,
annotation.handleClasses());
}
for (Class<?> innerClass : bean.getClass().getDeclaredClasses()) {
if (!Modifier.isAbstract(innerClass.getModifiers())
&& (JsonSerializer.class.isAssignableFrom(innerClass)
|| JsonDeserializer.class.isAssignableFrom(innerClass))) {
|| JsonDeserializer.class.isAssignableFrom(innerClass)
|| KeyDeserializer.class.isAssignableFrom(innerClass))) {
try {
addJsonBean(innerClass.newInstance());
addJsonBean(innerClass.newInstance(), annotation);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
Expand All @@ -93,17 +105,54 @@ private void addJsonBean(Object bean) {
}

@SuppressWarnings({ "unchecked" })
private <T> void addSerializerWithDeducedType(JsonSerializer<T> serializer) {
ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
serializer.getClass());
addSerializer((Class<T>) type.resolveGeneric(), serializer);
private <T> void addSerializerForTypes(JsonSerializer<T> serializer,
JsonComponent.Handle handle, Class<?>[] types) {
for (Class<?> type : types) {
addSerializerWithType(serializer, handle, (Class<T>) type);
}

if (types.length == 0) {
ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
serializer.getClass());
addSerializerWithType(serializer, handle, (Class<T>) type.resolveGeneric());
}
}

private <T> void addSerializerWithType(JsonSerializer<T> serializer,
JsonComponent.Handle handle, Class<? extends T> type) {
if (JsonComponent.Handle.KEYS.equals(handle)) {
addKeySerializer(type, serializer);
}
else {
addSerializer(type, serializer);
}
}

@SuppressWarnings({ "unchecked" })
private <T> void addDeserializerForTypes(JsonDeserializer<T> deserializer,
Class<?>[] types) {
for (Class<?> type : types) {
addDeserializer((Class<T>) type, deserializer);
}

if (types.length == 0) {
addDeserializerWithDeducedType(deserializer);
}
}

@SuppressWarnings({ "unchecked" })
private <T> void addDeserializerWithDeducedType(JsonDeserializer<T> deserializer) {
ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
deserializer.getClass());
addDeserializer((Class<T>) type.resolveGeneric(), deserializer);

}

private void addKeyDeserializerForTypes(KeyDeserializer deserializer,
Class<?>[] types) {
for (Class<?> type : types) {
addKeyDeserializer(type, deserializer);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@

package org.springframework.boot.jackson;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
Expand All @@ -24,12 +30,14 @@
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

/**
* Tests for {@link JsonComponentModule}.
*
* @author Phillip Webb
* @author Vladimir Tsanev
* @author Paul Aly
*/
public class JsonComponentModuleTests {

Expand Down Expand Up @@ -73,6 +81,38 @@ public void moduleShouldAllowInnerAbstractClasses() throws Exception {
context.close();
}

@Test
public void moduleShouldRegisterKeySerializers() throws Exception {
load(OnlyKeySerializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeySerialize(module);
}

@Test
public void moduleShouldRegisterKeyDeserializers() throws Exception {
load(OnlyKeyDeserializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeyDeserialize(module);
}

@Test
public void moduleShouldRegisterInnerClassesForKeyHandlers() throws Exception {
load(NameAndAgeJsonKeyComponent.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeySerialize(module);
assertKeyDeserialize(module);
}

@Test
public void moduleShouldRegisterOnlyForSpecifiedClasses() throws Exception {
load(NameAndCareerJsonComponent.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertSerialize(module, new NameAndCareer("spring", "developer"),
"{\"name\":\"spring\"}");
assertSerialize(module);
assertDeserializeForSpecifiedClasses(module);
}

private void load(Class<?>... configs) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configs);
Expand All @@ -81,11 +121,17 @@ private void load(Class<?>... configs) {
this.context = context;
}

private void assertSerialize(Module module) throws Exception {
private void assertSerialize(Module module, Name value, String expectedJson)
throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(new NameAndAge("spring", 100));
assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}");
String json = mapper.writeValueAsString(value);
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
}

private void assertSerialize(Module module) throws Exception {
assertSerialize(module, new NameAndAge("spring", 100),
"{\"name\":\"spring\",\"age\":100}");
}

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

private void assertDeserializeForSpecifiedClasses(JsonComponentModule module)
throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
assertThatExceptionOfType(JsonMappingException.class).isThrownBy(() -> mapper
.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class));
NameAndCareer nameAndCareer = mapper.readValue(
"{\"name\":\"spring\",\"career\":\"developer\"}", NameAndCareer.class);
assertThat(nameAndCareer.getName()).isEqualTo("spring");
assertThat(nameAndCareer.getCareer()).isEqualTo("developer");
}

private void assertKeySerialize(Module module) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Map<NameAndAge, Boolean> map = new HashMap<>();
map.put(new NameAndAge("spring", 100), true);
String json = mapper.writeValueAsString(map);
assertThat(json).isEqualToIgnoringWhitespace("{\"spring is 100\": true}");
}

private void assertKeyDeserialize(Module module) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
TypeReference<Map<NameAndAge, Boolean>> typeRef = new TypeReference<Map<NameAndAge, Boolean>>() {
};
Map<NameAndAge, Boolean> map = mapper.readValue("{\"spring is 100\": true}",
typeRef);
assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
}

@JsonComponent
static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {

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

}

@JsonComponent(handle = JsonComponent.Handle.KEYS)
static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {

}

@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2012-2017 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.boot.jackson;

/**
* Sample object used for tests.
*
* @author Paul Aly
*/
public class Name {

protected final String name;

public Name(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

}
Loading