From 63420fe2f6d510f200f6ae8f5bd0bbde7218067b Mon Sep 17 00:00:00 2001 From: maly7 Date: Tue, 9 Apr 2019 06:44:48 -0400 Subject: [PATCH] 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 77b74582b180..2732cd60b3bb 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 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 @@ /** * {@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 @@
 	@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 f5739a6a3b59..4f9145293e28 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 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.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 @@ private void addJsonBeans(ListableBeanFactory beanFactory) {
 		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 @@ private void addJsonBean(Object bean) {
 	}
 
 	@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 @@ private  void addDeserializerWithDeducedType(JsonDeserializer deserializer
 		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 035cb54e1c6d..3b2d01e42922 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.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 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);
@@ -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 {
@@ -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 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 @@ 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 {
+
+	}
+
 }
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 000000000000..bfbd02ecf2fe
--- /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 c44148deb953..f343c1cc2a64 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 000000000000..bda3f435a1c2
--- /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 000000000000..2a8dc1e0002c
--- /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 000000000000..778705d88359
--- /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);
+		}
+
+	}
+
+}