diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index 346cc48a4c..a58987c8f7 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -46,6 +46,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnJava; import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.JavaVersion; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -75,6 +76,11 @@ import org.springframework.util.ReflectionUtils; @ConditionalOnClass(ObjectMapper.class) public class JacksonAutoConfiguration { + @Bean + public JsonComponentModule jsonComponentModule() { + return new JsonComponentModule(); + } + @Configuration @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) static class JacksonObjectMapperConfiguration { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 1a07a7388e..252e0a4498 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -50,10 +50,13 @@ import org.junit.Before; import org.junit.Test; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.jackson.JsonComponent; +import org.springframework.boot.jackson.JsonObjectSerializer; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -347,6 +350,7 @@ public class JacksonAutoConfigurationTests { assertThat(this.context.getBean(CustomModule.class).getOwners()) .contains((ObjectCodec) objectMapper); assertThat(objectMapper.canSerialize(LocalDateTime.class)).isTrue(); + assertThat(objectMapper.canSerialize(Baz.class)).isTrue(); } @Test @@ -456,12 +460,14 @@ public class JacksonAutoConfigurationTests { } @Configuration + @Import(BazSerializer.class) protected static class ModuleConfig { @Bean public CustomModule jacksonModule() { return new CustomModule(); } + } @Configuration @@ -538,6 +544,20 @@ public class JacksonAutoConfigurationTests { } } + @JsonComponent + private static class BazSerializer extends JsonObjectSerializer { + + @Override + protected void serializeObject(Baz value, JsonGenerator jgen, + SerializerProvider provider) throws IOException { + } + + } + + private static class Baz { + + } + private static class CustomModule extends SimpleModule { private Set owners = new HashSet(); @@ -552,4 +572,5 @@ public class JacksonAutoConfigurationTests { } } + } diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 959b5cb5e8..a5032c56fc 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -1470,6 +1470,51 @@ converters. You can also override default converters that way. +[[boot-features-json-components]] +==== Custom JSON Serializers and Deserializers +If you're using Jackson to serialize and deserialize JSON data, you might want to write +your own `JsonSerializer` and `JsonDeserializer` classes. Custom serializers are usually +http://wiki.fasterxml.com/JacksonHowToCustomDeserializers[registered with Jackson via a Module], +but Spring Boot provides an alternative `@JsonComponent` annotation which makes it easier +to directly register Spring Beans. + +You can use `@JsonComponent` directly on `JsonSerializer` or `JsonDeserializer` +implementations. You can also use it on classes that contains serializers/deserializers as +inner-classes. For example: + +[source,java,indent=0] +---- + import java.io.*; + import com.fasterxml.jackson.core.*; + import com.fasterxml.jackson.databind.*; + import org.springframework.boot.jackson.*; + + @JsonComponent + public class Example { + + public static class Serializer extends JsonSerializer { + // ... + } + + public static class Deserializer extends JsonDeserializer { + // ... + } + + } +---- + +All `@JsonComponent` beans in the `ApplicationContext` will be automatically registered with +Jackson, and since `@JsonComponent` is meta-annotated with `@Component`, the usual +component-scanning rules apply. + +Spring Boot also provides +{sc-spring-boot}/jackson/JsonObjectSerializer.{sc-ext}[`JsonObjectSerializer`] and +{sc-spring-boot}/jackson/JsonObjectDeserializer.{sc-ext}[`JsonObjectDeserializer`] base +classes which provide useful alternatives to the standard Jackson versions when +serializing Objects. See the Javadoc for details. + + + [[boot-features-spring-message-codes]] ==== MessageCodesResolver Spring MVC has a strategy for generating error codes for rendering error messages diff --git a/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java new file mode 100644 index 0000000000..054348c66d --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponent.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; + +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: + *
+ * @JsonComponent
+ * public class CustomerJsonComponent {
+ *
+ *     public static class Serializer extends JsonSerializer<Customer> {
+ *
+ *         // ...
+ *
+ *     }
+ *
+ *     public static class Deserializer extends JsonDeserializer<Customer> {
+ *
+ *         // ...
+ *
+ *     }
+ *
+ * }
+ *
+ * 
+ * + * @see JsonComponentModule + * @since 1.4.0 + * @author Phillip Webb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface JsonComponent { + + /** + * The value may indicate a suggestion for a logical component name, to be turned into + * a Spring bean in case of an autodetected component. + */ + String value() default ""; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java new file mode 100644 index 0000000000..33f9041ca9 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonComponentModule.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.util.Map; + +import javax.annotation.PostConstruct; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; + +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.core.ResolvableType; + +/** + * Spring Bean and Jackson {@link Module} to register {@link JsonComponent} annotated + * beans. + * + * @author Phillip Webb + * @since 1.4.0 + * @see JsonComponent + */ +public class JsonComponentModule extends SimpleModule implements BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @PostConstruct + public void registerJsonComponents() { + BeanFactory beanFactory = this.beanFactory; + while (beanFactory != null) { + if (beanFactory instanceof ListableBeanFactory) { + addJsonBeans((ListableBeanFactory) beanFactory); + } + beanFactory = (beanFactory instanceof HierarchicalBeanFactory + ? ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory() + : null); + } + } + + private void addJsonBeans(ListableBeanFactory beanFactory) { + Map beans = beanFactory + .getBeansWithAnnotation(JsonComponent.class); + for (Object bean : beans.values()) { + addJsonBean(bean); + } + } + + private void addJsonBean(Object bean) { + if (bean instanceof JsonSerializer) { + addSerializerWithDeducedType((JsonSerializer) bean); + } + if (bean instanceof JsonDeserializer) { + addDeserializerWithDeducedType((JsonDeserializer) bean); + } + for (Class innerClass : bean.getClass().getDeclaredClasses()) { + if (JsonSerializer.class.isAssignableFrom(innerClass) + || JsonDeserializer.class.isAssignableFrom(innerClass)) { + try { + addJsonBean(innerClass.newInstance()); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + } + + @SuppressWarnings({ "unchecked" }) + private void addSerializerWithDeducedType(JsonSerializer serializer) { + ResolvableType type = ResolvableType.forClass(JsonSerializer.class, + serializer.getClass()); + addSerializer((Class) type.resolveGeneric(), serializer); + } + + @SuppressWarnings({ "unchecked" }) + private void addDeserializerWithDeducedType(JsonDeserializer deserializer) { + ResolvableType type = ResolvableType.forClass(JsonDeserializer.class, + deserializer.getClass()); + addDeserializer((Class) type.resolveGeneric(), deserializer); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectDeserializer.java b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectDeserializer.java new file mode 100644 index 0000000000..1e45d672d2 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectDeserializer.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 java.math.BigDecimal; +import java.math.BigInteger; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import org.springframework.util.Assert; + +/** + * Helper base class for {@link JsonDeserializer} implementations that deserialize + * objects. + * + * @param The supported object type + * @author Phillip Webb + * @since 1.4.0 + * @see JsonObjectSerializer + */ +public abstract class JsonObjectDeserializer + extends com.fasterxml.jackson.databind.JsonDeserializer { + + @Override + public final T deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + try { + ObjectCodec codec = jp.getCodec(); + JsonNode tree = codec.readTree(jp); + return deserializeObject(jp, ctxt, codec, tree); + } + catch (Exception ex) { + if (ex instanceof IOException) { + throw (IOException) ex; + } + throw new JsonMappingException(jp, "Object deserialize error", ex); + } + } + + /** + * Deserialize JSON content into the value type this serializer handles. + * @param jasonParser the source parser used for reading JSON content + * @param context Context that can be used to access information about this + * deserialization activity + * @param codec the {@link ObjectCodec} associated with the parser + * @param tree deserialized JSON content as tree expressed using set of + * {@link TreeNode} instances + * @return the dserialized object + * @throws IOException on error + * @see #deserialize(JsonParser, DeserializationContext) + */ + protected abstract T deserializeObject(JsonParser jasonParser, + DeserializationContext context, ObjectCodec codec, JsonNode tree) + throws IOException; + + /** + * Helper method to extract a value from the given jsonNode or return {@code null} + * when the node itself is {@code null}. + * @param jsonNode the source node (may be {@code null} + * @param type the data type. May be {@link String}, {@link Boolean}, {@link Long}, + * {@link Integer}, {@link Short}, {@link Double}, {@link Float}, {@link BigDecimal} + * or {@link BigInteger}. + * @param the data type requested + * @return the node value or {@code null} + */ + @SuppressWarnings({ "unchecked" }) + protected final D nullSafeValue(JsonNode jsonNode, Class type) { + Assert.notNull(type, "Type must not be null"); + if (jsonNode == null) { + return null; + } + if (type == String.class) { + return (D) jsonNode.textValue(); + } + if (type == Boolean.class) { + return (D) Boolean.valueOf(jsonNode.booleanValue()); + } + if (type == Long.class) { + return (D) Long.valueOf(jsonNode.longValue()); + } + if (type == Integer.class) { + return (D) Integer.valueOf(jsonNode.intValue()); + } + if (type == Short.class) { + return (D) Short.valueOf(jsonNode.shortValue()); + } + if (type == Double.class) { + return (D) Double.valueOf(jsonNode.doubleValue()); + } + if (type == Float.class) { + return (D) Float.valueOf(jsonNode.floatValue()); + } + if (type == BigDecimal.class) { + return (D) jsonNode.decimalValue(); + } + if (type == BigInteger.class) { + return (D) jsonNode.bigIntegerValue(); + } + throw new IllegalArgumentException("Unsupported value type " + type.getName()); + } + + /** + * Helper method to return a {@link JsonNode} from the tree. + * @param tree the source tree + * @param fieldName the field name to extract + * @return the {@link JsonNode} + */ + protected final JsonNode getRequiredNode(JsonNode tree, String fieldName) { + Assert.notNull(tree, "Tree must not be null"); + JsonNode node = tree.get(fieldName); + Assert.state(node != null && !(node instanceof NullNode), + "Missing JSON field '" + fieldName + "'"); + return node; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectSerializer.java b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectSerializer.java new file mode 100644 index 0000000000..552a996385 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectSerializer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * Helper base class for {@link JsonSerializer} implementations that serialize objects. + * + * @param the supported object type + * @author Phillip Webb + * @since 1.4.0 + * @see JsonObjectDeserializer + */ +public abstract class JsonObjectSerializer extends JsonSerializer { + + @Override + public final void serialize(T value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + try { + jgen.writeStartObject(); + serializeObject(value, jgen, provider); + jgen.writeEndObject(); + } + catch (Exception ex) { + if (ex instanceof IOException) { + throw (IOException) ex; + } + throw new JsonMappingException(jgen, "Object serialize error", ex); + } + } + + /** + * Serialize JSON content into the value type this serializer handles. + * @param value the source value + * @param jgen the JSON generator + * @param provider the serializer provider + * @throws IOException on error + */ + protected abstract void serializeObject(T value, JsonGenerator jgen, + SerializerProvider provider) throws IOException; + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/jackson/package-info.java b/spring-boot/src/main/java/org/springframework/boot/jackson/package-info.java new file mode 100644 index 0000000000..328b2d70aa --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2016 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 + * + * http://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. + */ + +/** + * Custom enhancements and support for the Jackson Project. + */ +package org.springframework.boot.jackson; diff --git a/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java new file mode 100644 index 0000000000..03d3b78d59 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonComponentModuleTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonComponentModule}. + * + * @author Phillip Webb + */ +public class JsonComponentModuleTests { + + @Test + public void moduleShouldRegisterSerializers() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + JsonComponentModule.class, OnlySerializer.class); + JsonComponentModule module = context.getBean(JsonComponentModule.class); + assertSerialize(module); + context.close(); + } + + @Test + public void moduleShouldRegisterDeserializers() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + JsonComponentModule.class, OnlyDeserializer.class); + JsonComponentModule module = context.getBean(JsonComponentModule.class); + assertDeserialize(module); + context.close(); + } + + @Test + public void moduleShouldRegisterInnerClasses() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + JsonComponentModule.class, NameAndAgeJsonComponent.class); + JsonComponentModule module = context.getBean(JsonComponentModule.class); + assertSerialize(module); + assertDeserialize(module); + context.close(); + } + + private void assertSerialize(Module module) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + String json = mapper.writeValueAsString(new NameAndAge("spring", 100)); + assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}"); + } + + private void assertDeserialize(Module module) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + NameAndAge nameAndAge = mapper.readValue("{\"name\":\"spring\",\"age\":100}", + NameAndAge.class); + assertThat(nameAndAge.getName()).isEqualTo("spring"); + assertThat(nameAndAge.getAge()).isEqualTo(100); + } + + @JsonComponent + static class OnlySerializer extends NameAndAgeJsonComponent.Serializer { + + } + + @JsonComponent + static class OnlyDeserializer extends NameAndAgeJsonComponent.Deserializer { + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectDeserializerTests.java b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectDeserializerTests.java new file mode 100644 index 0000000000..6192db7671 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectDeserializerTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; + +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.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.NullNode; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.jackson.NameAndAgeJsonComponent.Deserializer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JsonObjectDeserializer}. + * + * @author Phillip Webb + */ +public class JsonObjectDeserializerTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private TestJsonObjectDeserializer testDeserializer = new TestJsonObjectDeserializer(); + + @Test + public void deserializeObjectShouldReadJson() throws Exception { + Deserializer deserializer = new NameAndAgeJsonComponent.Deserializer(); + SimpleModule module = new SimpleModule(); + module.addDeserializer(NameAndAge.class, deserializer); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + NameAndAge nameAndAge = mapper.readValue("{\"name\":\"spring\",\"age\":100}", + NameAndAge.class); + assertThat(nameAndAge.getName()).isEqualTo("spring"); + assertThat(nameAndAge.getAge()).isEqualTo(100); + } + + @Test + public void nullSafeValueWhenValueIsNullShouldReturnNull() throws Exception { + String value = this.testDeserializer.testNullSafeValue(null, String.class); + assertThat(value).isNull(); + } + + @Test + public void nullSafeValueWhenClassIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Type must not be null"); + this.testDeserializer.testNullSafeValue(mock(JsonNode.class), null); + } + + @Test + public void nullSafeValueWhenClassIsStringShouldReturnString() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.textValue()).willReturn("abc"); + String value = this.testDeserializer.testNullSafeValue(node, String.class); + assertThat(value).isEqualTo("abc"); + } + + @Test + public void nullSafeValueWhenClassIsBooleanShouldReturnBoolean() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.booleanValue()).willReturn(true); + Boolean value = this.testDeserializer.testNullSafeValue(node, Boolean.class); + assertThat(value).isTrue(); + } + + @Test + public void nullSafeValueWhenClassIsLongShouldReturnLong() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.longValue()).willReturn(10L); + Long value = this.testDeserializer.testNullSafeValue(node, Long.class); + assertThat(value).isEqualTo(10L); + } + + @Test + public void nullSafeValueWhenClassIsIntegerShouldReturnInteger() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.intValue()).willReturn(10); + Integer value = this.testDeserializer.testNullSafeValue(node, Integer.class); + assertThat(value).isEqualTo(10); + } + + @Test + public void nullSafeValueWhenClassIsShortShouldReturnShort() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.shortValue()).willReturn((short) 10); + Short value = this.testDeserializer.testNullSafeValue(node, Short.class); + assertThat(value).isEqualTo((short) 10); + } + + @Test + public void nullSafeValueWhenClassIsDoubleShouldReturnDouble() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.doubleValue()).willReturn(1.1D); + Double value = this.testDeserializer.testNullSafeValue(node, Double.class); + assertThat(value).isEqualTo(1.1D); + } + + @Test + public void nullSafeValueWhenClassIsFloatShouldReturnFloat() throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.floatValue()).willReturn(1.1F); + Float value = this.testDeserializer.testNullSafeValue(node, Float.class); + assertThat(value).isEqualTo(1.1F); + } + + @Test + public void nullSafeValueWhenClassIsShouldBigDecimalReturnBigDecimal() + throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.decimalValue()).willReturn(BigDecimal.TEN); + BigDecimal value = this.testDeserializer.testNullSafeValue(node, + BigDecimal.class); + assertThat(value).isEqualTo(BigDecimal.TEN); + } + + @Test + public void nullSafeValueWhenClassIsBigIntegerShouldReturnBigInteger() + throws Exception { + JsonNode node = mock(JsonNode.class); + given(node.bigIntegerValue()).willReturn(BigInteger.TEN); + BigInteger value = this.testDeserializer.testNullSafeValue(node, + BigInteger.class); + assertThat(value).isEqualTo(BigInteger.TEN); + } + + @Test + public void nullSafeValueWhenClassIsUnknownShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Unsupported value type java.io.InputStream"); + this.testDeserializer.testNullSafeValue(mock(JsonNode.class), InputStream.class); + + } + + @Test + public void getRequiredNodeWhenTreeIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Tree must not be null"); + this.testDeserializer.testGetRequiredNode(null, "test"); + } + + @Test + public void getRequiredNodeWhenNodeIsNullShouldThrowException() throws Exception { + JsonNode tree = mock(JsonNode.class); + given(tree.get("test")).willReturn(null); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Missing JSON field 'test'"); + this.testDeserializer.testGetRequiredNode(tree, "test"); + } + + @Test + public void getRequiredNodeWhenNodeIsNullNodeShouldThrowException() throws Exception { + JsonNode tree = mock(JsonNode.class); + given(tree.get("test")).willReturn(NullNode.instance); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Missing JSON field 'test'"); + this.testDeserializer.testGetRequiredNode(tree, "test"); + } + + @Test + public void getRequiredNodeWhenNodeIsFoundShouldReturnNode() throws Exception { + JsonNode node = mock(JsonNode.class); + JsonNode tree = node; + given(tree.get("test")).willReturn(node); + assertThat(this.testDeserializer.testGetRequiredNode(tree, "test")) + .isEqualTo(node); + } + + static class TestJsonObjectDeserializer extends JsonObjectDeserializer { + + @Override + protected T deserializeObject(JsonParser jasonParser, + DeserializationContext context, ObjectCodec codec, JsonNode tree) + throws IOException { + return null; + } + + public D testNullSafeValue(JsonNode jsonNode, Class type) { + return nullSafeValue(jsonNode, type); + } + + public JsonNode testGetRequiredNode(JsonNode tree, String fieldName) { + return getRequiredNode(tree, fieldName); + } + + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectSerializerTests.java b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectSerializerTests.java new file mode 100644 index 0000000000..f02b8352c6 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jackson/JsonObjectSerializerTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.Test; + +import org.springframework.boot.jackson.NameAndAgeJsonComponent.Serializer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonObjectSerializer}. + * + * @author Phillip Webb + */ +public class JsonObjectSerializerTests { + + @Test + public void serializeObjectShouldWriteJson() throws Exception { + Serializer serializer = new NameAndAgeJsonComponent.Serializer(); + SimpleModule module = new SimpleModule(); + module.addSerializer(NameAndAge.class, serializer); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + String json = mapper.writeValueAsString(new NameAndAge("spring", 100)); + assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}"); + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java b/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java new file mode 100644 index 0000000000..eb5a3b8f3f --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAge.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 Phillip Webb + */ +public final class NameAndAge { + + private final String name; + + private final int age; + + public NameAndAge(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return this.name; + } + + public int getAge() { + return this.age; + } + +} diff --git a/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonComponent.java b/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonComponent.java new file mode 100644 index 0000000000..d8fb9f65f1 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/jackson/NameAndAgeJsonComponent.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2016 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 + * + * http://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 Phillip Webb + */ +@JsonComponent +public class NameAndAgeJsonComponent { + + public static class Serializer extends JsonObjectSerializer { + + @Override + protected void serializeObject(NameAndAge value, JsonGenerator jgen, + SerializerProvider provider) throws IOException { + jgen.writeStringField("name", value.getName()); + jgen.writeNumberField("age", value.getAge()); + } + + } + + public static class Deserializer extends JsonObjectDeserializer { + + @Override + protected NameAndAge deserializeObject(JsonParser jasonParser, + DeserializationContext context, ObjectCodec codec, JsonNode tree) + throws IOException { + String name = nullSafeValue(tree.get("name"), String.class); + Integer age = nullSafeValue(tree.get("age"), Integer.class); + return new NameAndAge(name, age); + } + + } + +}