From 361efc7c11dd5679150ee649059039fa313677ad Mon Sep 17 00:00:00 2001 From: maly7 Date: Tue, 9 Apr 2019 06:44:48 -0400 Subject: [PATCH 1/2] Support JsonComponent key serializers/deserialzers Update `@JsonComponent` so that it can also be used to register key serializers and deserializers. See gh-16544 --- .../boot/jackson/JsonComponent.java | 40 +++++++- .../boot/jackson/JsonComponentModule.java | 69 ++++++++++++-- .../jackson/JsonComponentModuleTests.java | 93 ++++++++++++++++++- .../springframework/boot/jackson/Name.java | 36 +++++++ .../boot/jackson/NameAndAge.java | 45 +++++++-- .../jackson/NameAndAgeJsonKeyComponent.java | 56 +++++++++++ .../boot/jackson/NameAndCareer.java | 37 ++++++++ .../jackson/NameAndCareerJsonComponent.java | 59 ++++++++++++ 8 files changed, 411 insertions(+), 24 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/Name.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java index 77b74582b1..2732cd60b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java @@ -24,6 +24,7 @@ import java.lang.annotation.Target; 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; @@ -31,9 +32,9 @@ 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: - *
+ * 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: 
  * @JsonComponent
  * public class CustomerJsonComponent {
  *
@@ -71,4 +72,37 @@ public @interface JsonComponent {
 	@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
+
+	}
+
 }
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
index f5739a6a3b..4f9145293e 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
@@ -23,6 +23,7 @@ import javax.annotation.PostConstruct;
 
 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;
 
@@ -32,12 +33,14 @@ import org.springframework.beans.factory.BeanFactoryAware;
 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
  */
@@ -67,23 +70,32 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
 		Map 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);
@@ -93,10 +105,39 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
 	}
 
 	@SuppressWarnings({ "unchecked" })
-	private  void addSerializerWithDeducedType(JsonSerializer serializer) {
-		ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
-				serializer.getClass());
-		addSerializer((Class) type.resolveGeneric(), serializer);
+	private  void addSerializerForTypes(JsonSerializer serializer,
+			JsonComponent.Handle handle, Class[] types) {
+		for (Class type : types) {
+			addSerializerWithType(serializer, handle, (Class) type);
+		}
+
+		if (types.length == 0) {
+			ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
+					serializer.getClass());
+			addSerializerWithType(serializer, handle, (Class) type.resolveGeneric());
+		}
+	}
+
+	private  void addSerializerWithType(JsonSerializer serializer,
+			JsonComponent.Handle handle, Class type) {
+		if (JsonComponent.Handle.KEYS.equals(handle)) {
+			addKeySerializer(type, serializer);
+		}
+		else {
+			addSerializer(type, serializer);
+		}
+	}
+
+	@SuppressWarnings({ "unchecked" })
+	private  void addDeserializerForTypes(JsonDeserializer deserializer,
+			Class[] types) {
+		for (Class type : types) {
+			addDeserializer((Class) type, deserializer);
+		}
+
+		if (types.length == 0) {
+			addDeserializerWithDeducedType(deserializer);
+		}
 	}
 
 	@SuppressWarnings({ "unchecked" })
@@ -104,6 +145,14 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
 		ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
 				deserializer.getClass());
 		addDeserializer((Class) type.resolveGeneric(), deserializer);
+
+	}
+
+	private void addKeyDeserializerForTypes(KeyDeserializer deserializer,
+			Class[] types) {
+		for (Class type : types) {
+			addKeyDeserializer(type, deserializer);
+		}
 	}
 
 }
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
index 035cb54e1c..3b2d01e429 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
@@ -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;
@@ -24,12 +30,14 @@ import org.junit.Test;
 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 {
 
@@ -73,6 +81,38 @@ public class JsonComponentModuleTests {
 		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);
@@ -81,11 +121,17 @@ public class JsonComponentModuleTests {
 		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 {
@@ -97,6 +143,37 @@ public class JsonComponentModuleTests {
 		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 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> typeRef = new TypeReference>() {
+		};
+		Map map = mapper.readValue("{\"spring is 100\":  true}",
+				typeRef);
+		assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
+	}
+
 	@JsonComponent
 	static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {
 
@@ -121,4 +198,14 @@ public class JsonComponentModuleTests {
 
 	}
 
+	@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 {
+
+	}
+
 }
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/Name.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/Name.java
new file mode 100644
index 0000000000..bfbd02ecf2
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/Name.java
@@ -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;
+	}
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
index c44148deb9..f343c1cc2a 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
@@ -16,28 +16,57 @@
 
 package org.springframework.boot.jackson;
 
+import org.springframework.util.ObjectUtils;
+
 /**
  * Sample object used for tests.
  *
  * @author Phillip Webb
  */
-public final class NameAndAge {
-
-	private final String name;
+public final class NameAndAge extends Name {
 
 	private final int age;
 
 	public NameAndAge(String name, int age) {
-		this.name = name;
+		super(name);
 		this.age = age;
 	}
 
-	public String getName() {
-		return this.name;
-	}
-
 	public int getAge() {
 		return this.age;
 	}
 
+	public String asKey() {
+		return name + " is " + age;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null) {
+			return false;
+		}
+
+		if (obj instanceof NameAndAge) {
+			NameAndAge other = (NameAndAge) obj;
+			boolean rtn = true;
+			rtn = rtn && ObjectUtils.nullSafeEquals(this.name, other.name);
+			rtn = rtn && ObjectUtils.nullSafeEquals(this.age, other.age);
+			return rtn;
+		}
+
+		return super.equals(obj);
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ObjectUtils.nullSafeHashCode(this.name);
+		result = prime * result + ObjectUtils.nullSafeHashCode(this.age);
+		return result;
+	}
+
 }
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
new file mode 100644
index 0000000000..bda3f435a1
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.KeyDeserializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * Sample {@link JsonComponent} used for tests.
+ *
+ * @author Paul Aly
+ */
+@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
+public class NameAndAgeJsonKeyComponent {
+
+	public static class Serializer extends JsonSerializer {
+
+		@Override
+		public void serialize(NameAndAge value, JsonGenerator jgen,
+				SerializerProvider serializers) throws IOException {
+			jgen.writeFieldName(value.asKey());
+		}
+
+	}
+
+	public static class Deserializer extends KeyDeserializer {
+
+		@Override
+		public NameAndAge deserializeKey(String key, DeserializationContext ctxt)
+				throws IOException {
+			String[] keys = key.split("is");
+			return new NameAndAge(keys[0].trim(), Integer.valueOf(keys[1].trim()));
+		}
+
+	}
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareer.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareer.java
new file mode 100644
index 0000000000..2a8dc1e000
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareer.java
@@ -0,0 +1,37 @@
+/*
+ * 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 NameAndCareer extends Name {
+
+	private final String career;
+
+	public NameAndCareer(String name, String career) {
+		super(name);
+		this.career = career;
+	}
+
+	public String getCareer() {
+		return this.career;
+	}
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
new file mode 100644
index 0000000000..778705d883
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+/**
+ * Sample {@link JsonComponent} used for tests.
+ *
+ * @author Paul Aly
+ */
+@JsonComponent(handleClasses = NameAndCareer.class)
+public class NameAndCareerJsonComponent {
+
+	public static class Serializer extends JsonObjectSerializer {
+
+		@Override
+		protected void serializeObject(Name value, JsonGenerator jgen,
+				SerializerProvider provider) throws IOException {
+			jgen.writeStringField("name", value.getName());
+		}
+
+	}
+
+	public static class Deserializer extends JsonObjectDeserializer {
+
+		@Override
+		protected Name deserializeObject(JsonParser jsonParser,
+				DeserializationContext context, ObjectCodec codec, JsonNode tree)
+				throws IOException {
+			String name = nullSafeValue(tree.get("name"), String.class);
+			String career = nullSafeValue(tree.get("career"), String.class);
+			return new NameAndCareer(name, career);
+		}
+
+	}
+
+}

From bf633fc5754f455cd9317f095b9cda967b5ed31f Mon Sep 17 00:00:00 2001
From: Phillip Webb 
Date: Sun, 2 Jun 2019 17:29:17 -0700
Subject: [PATCH 2/2] Polish "Support JsonComponent key
 serializers/deserialzers"

See gh-16544
---
 .../boot/jackson/JsonComponent.java           |  53 ++++----
 .../boot/jackson/JsonComponentModule.java     | 121 +++++++++---------
 .../jackson/JsonComponentModuleTests.java     |   6 +-
 .../boot/jackson/NameAndAge.java              |   7 +-
 .../jackson/NameAndAgeJsonKeyComponent.java   |   4 +-
 .../jackson/NameAndCareerJsonComponent.java   |   4 +-
 6 files changed, 99 insertions(+), 96 deletions(-)

diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java
index 2732cd60b3..e11e0716d4 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -30,11 +30,10 @@ 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}, {@link JsonDeserializer}, or
- * {@link KeyDeserializer} implementations directly or a class that contains them as
- * inner-classes. For example: 
+ * {@link Component} that provides {@link JsonSerializer}, {@link JsonDeserializer} or
+ * {@link KeyDeserializer} implementations to be registered with Jackson when
+ * {@link JsonComponentModule} is in use. Can be used to annotate implementations directly
+ * or a class that contains them as inner-classes. For example: 
  * @JsonComponent
  * public class CustomerJsonComponent {
  *
@@ -57,6 +56,7 @@ import org.springframework.stereotype.Component;
  * @see JsonComponentModule
  * @since 1.4.0
  * @author Phillip Webb
+ * @author Paul Aly
  */
 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
@@ -73,33 +73,40 @@ public @interface JsonComponent {
 	String value() default "";
 
 	/**
-	 * Indicates whether the component should be registered as a type serializer and/or
-	 * deserializer or a key serializer and/or deserializer.
+	 * The types that are handled by the provided serializer/deserializer. This attribute
+	 * is mandatory for a {@link KeyDeserializer}, as the type cannot be inferred. For a
+	 * {@link JsonSerializer} or {@link JsonDeserializer} it can be used to limit handling
+	 * to a subclasses of type inferred from the generic.
+	 * @return the types that should be handled by the component
+	 * @since 2.2.0
+	 */
+	Class[] type() default {};
+
+	/**
+	 * The scope under which the serializer/deserializer should be registered with the
+	 * module.
 	 * @return the component's handle type
+	 * @since 2.2.0
 	 */
-	Handle handle() default Handle.TYPES;
+	Scope scope() default Scope.VALUES;
 
 	/**
-	 * 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
+	 * The various scopes under which a serializer/deserialzier can be registered.
+	 * @since 2.2.0
 	 */
-	Class[] handleClasses() default {};
-
-	/**
-	 * An enumeration of possible handling types for the component.
-	 */
-	enum Handle {
+	enum Scope {
 
 		/**
-		 * Register the component as a Type serializer and/or deserializer.
+		 * A serializer/deserializer for regular value content.
+		 * @see JsonSerializer
+		 * @see JsonDeserializer
 		 */
-		TYPES,
+		VALUES,
 
 		/**
-		 * Register the component as a Key serializer and/or deserializer.
+		 * A serializer/deserializer for keys.
+		 * @see JsonSerializer
+		 * @see KeyDeserializer
 		 */
 		KEYS
 
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
index 4f9145293e..750327900f 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2018 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -18,6 +18,7 @@ package org.springframework.boot.jackson;
 
 import java.lang.reflect.Modifier;
 import java.util.Map;
+import java.util.function.BiConsumer;
 
 import javax.annotation.PostConstruct;
 
@@ -27,13 +28,19 @@ import com.fasterxml.jackson.databind.KeyDeserializer;
 import com.fasterxml.jackson.databind.Module;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.BeanFactoryAware;
 import org.springframework.beans.factory.HierarchicalBeanFactory;
 import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.boot.jackson.JsonComponent.Scope;
 import org.springframework.core.ResolvableType;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.MergedAnnotation;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
 
 /**
  * Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated
@@ -70,88 +77,78 @@ public class JsonComponentModule extends SimpleModule implements BeanFactoryAwar
 		Map beans = beanFactory
 				.getBeansWithAnnotation(JsonComponent.class);
 		for (Object bean : beans.values()) {
-			JsonComponent annotation = AnnotationUtils.findAnnotation(bean.getClass(),
-					JsonComponent.class);
-			addJsonBean(bean, annotation);
+			addJsonBean(bean);
 		}
 	}
 
-	private void addJsonBean(Object bean, JsonComponent annotation) {
+	private void addJsonBean(Object bean) {
+		MergedAnnotation annotation = MergedAnnotations
+				.from(bean.getClass(), SearchStrategy.EXHAUSTIVE)
+				.get(JsonComponent.class);
+		Class[] types = annotation.getClassArray("type");
+		Scope scope = annotation.getEnum("scope", JsonComponent.Scope.class);
+		addJsonBean(bean, types, scope);
+	}
+
+	private void addJsonBean(Object bean, Class[] types, Scope scope) {
 		if (bean instanceof JsonSerializer) {
-			addSerializerForTypes((JsonSerializer) bean, annotation.handle(),
-					annotation.handleClasses());
+			addJsonSerializerBean((JsonSerializer) bean, scope, types);
 		}
-		if (bean instanceof KeyDeserializer) {
-			addKeyDeserializerForTypes((KeyDeserializer) bean,
-					annotation.handleClasses());
+		else if (bean instanceof JsonDeserializer) {
+			addJsonDeserializerBean((JsonDeserializer) bean, types);
 		}
-		if (bean instanceof JsonDeserializer) {
-			addDeserializerForTypes((JsonDeserializer) bean,
-					annotation.handleClasses());
+		else if (bean instanceof KeyDeserializer) {
+			addKeyDeserializerBean((KeyDeserializer) bean, types);
 		}
 		for (Class innerClass : bean.getClass().getDeclaredClasses()) {
-			if (!Modifier.isAbstract(innerClass.getModifiers())
-					&& (JsonSerializer.class.isAssignableFrom(innerClass)
-							|| JsonDeserializer.class.isAssignableFrom(innerClass)
-							|| KeyDeserializer.class.isAssignableFrom(innerClass))) {
-				try {
-					addJsonBean(innerClass.newInstance(), annotation);
-				}
-				catch (Exception ex) {
-					throw new IllegalStateException(ex);
-				}
+			if (isSuitableInnerClass(innerClass)) {
+				Object innerInstance = BeanUtils.instantiateClass(innerClass);
+				addJsonBean(innerInstance, types, scope);
 			}
 		}
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addSerializerForTypes(JsonSerializer serializer,
-			JsonComponent.Handle handle, Class[] types) {
-		for (Class type : types) {
-			addSerializerWithType(serializer, handle, (Class) type);
-		}
-
-		if (types.length == 0) {
-			ResolvableType type = ResolvableType.forClass(JsonSerializer.class,
-					serializer.getClass());
-			addSerializerWithType(serializer, handle, (Class) type.resolveGeneric());
-		}
+	private boolean isSuitableInnerClass(Class innerClass) {
+		return !Modifier.isAbstract(innerClass.getModifiers())
+				&& (JsonSerializer.class.isAssignableFrom(innerClass)
+						|| JsonDeserializer.class.isAssignableFrom(innerClass)
+						|| KeyDeserializer.class.isAssignableFrom(innerClass));
 	}
 
-	private  void addSerializerWithType(JsonSerializer serializer,
-			JsonComponent.Handle handle, Class type) {
-		if (JsonComponent.Handle.KEYS.equals(handle)) {
-			addKeySerializer(type, serializer);
-		}
-		else {
-			addSerializer(type, serializer);
-		}
+	@SuppressWarnings("unchecked")
+	private  void addJsonSerializerBean(JsonSerializer serializer,
+			JsonComponent.Scope scope, Class[] types) {
+		Class baseType = (Class) ResolvableType
+				.forClass(JsonSerializer.class, serializer.getClass()).resolveGeneric();
+		addBeanToModule(serializer, baseType, types,
+				(scope == Scope.VALUES) ? this::addSerializer : this::addKeySerializer);
+
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addDeserializerForTypes(JsonDeserializer deserializer,
+	@SuppressWarnings("unchecked")
+	private  void addJsonDeserializerBean(JsonDeserializer deserializer,
 			Class[] types) {
-		for (Class type : types) {
-			addDeserializer((Class) type, deserializer);
-		}
-
-		if (types.length == 0) {
-			addDeserializerWithDeducedType(deserializer);
-		}
+		Class baseType = (Class) ResolvableType
+				.forClass(JsonDeserializer.class, deserializer.getClass())
+				.resolveGeneric();
+		addBeanToModule(deserializer, baseType, types, this::addDeserializer);
 	}
 
-	@SuppressWarnings({ "unchecked" })
-	private  void addDeserializerWithDeducedType(JsonDeserializer deserializer) {
-		ResolvableType type = ResolvableType.forClass(JsonDeserializer.class,
-				deserializer.getClass());
-		addDeserializer((Class) type.resolveGeneric(), deserializer);
-
+	private void addKeyDeserializerBean(KeyDeserializer deserializer, Class[] types) {
+		Assert.notEmpty(types, "Type must be specified for KeyDeserializer");
+		addBeanToModule(deserializer, Object.class, types, this::addKeyDeserializer);
 	}
 
-	private void addKeyDeserializerForTypes(KeyDeserializer deserializer,
-			Class[] types) {
+	@SuppressWarnings("unchecked")
+	private  void addBeanToModule(E element, Class baseType, Class[] types,
+			BiConsumer, E> consumer) {
+		if (ObjectUtils.isEmpty(types)) {
+			consumer.accept(baseType, element);
+			return;
+		}
 		for (Class type : types) {
-			addKeyDeserializer(type, deserializer);
+			Assert.isAssignable(baseType, type);
+			consumer.accept((Class) type, element);
 		}
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
index 3b2d01e429..7a9af5f2df 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2018 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -198,12 +198,12 @@ public class JsonComponentModuleTests {
 
 	}
 
-	@JsonComponent(handle = JsonComponent.Handle.KEYS)
+	@JsonComponent(scope = JsonComponent.Scope.KEYS)
 	static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
 
 	}
 
-	@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
+	@JsonComponent(scope = JsonComponent.Scope.KEYS, type = NameAndAge.class)
 	static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
 
 	}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
index f343c1cc2a..7935b13723 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -22,6 +22,7 @@ import org.springframework.util.ObjectUtils;
  * Sample object used for tests.
  *
  * @author Phillip Webb
+ * @author Paul Aly
  */
 public final class NameAndAge extends Name {
 
@@ -37,7 +38,7 @@ public final class NameAndAge extends Name {
 	}
 
 	public String asKey() {
-		return name + " is " + age;
+		return this.name + " is " + this.age;
 	}
 
 	@Override
@@ -48,7 +49,6 @@ public final class NameAndAge extends Name {
 		if (obj == null) {
 			return false;
 		}
-
 		if (obj instanceof NameAndAge) {
 			NameAndAge other = (NameAndAge) obj;
 			boolean rtn = true;
@@ -56,7 +56,6 @@ public final class NameAndAge extends Name {
 			rtn = rtn && ObjectUtils.nullSafeEquals(this.age, other.age);
 			return rtn;
 		}
-
 		return super.equals(obj);
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
index bda3f435a1..10d2dc2d6b 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonKeyComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -29,7 +29,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
  *
  * @author Paul Aly
  */
-@JsonComponent(handle = JsonComponent.Handle.KEYS, handleClasses = NameAndAge.class)
+@JsonComponent(type = NameAndAge.class, scope = JsonComponent.Scope.KEYS)
 public class NameAndAgeJsonKeyComponent {
 
 	public static class Serializer extends JsonSerializer {
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
index 778705d883..9eeafc17a5 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndCareerJsonComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2017 the original author or authors.
+ * Copyright 2012-2019 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.
@@ -30,7 +30,7 @@ import com.fasterxml.jackson.databind.SerializerProvider;
  *
  * @author Paul Aly
  */
-@JsonComponent(handleClasses = NameAndCareer.class)
+@JsonComponent(type = NameAndCareer.class)
 public class NameAndCareerJsonComponent {
 
 	public static class Serializer extends JsonObjectSerializer {