diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index 14195ad22f7..84818ce0cdc 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -46,6 +46,7 @@ import org.springframework.core.log.LogFormatUtils; import org.springframework.http.codec.HttpMessageDecoder; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -126,7 +127,7 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple ObjectMapper mapper = selectObjectMapper(elementType, mimeType); if (mapper == null) { - throw new IllegalStateException("No ObjectMapper for " + elementType); + return Flux.error(new IllegalStateException("No ObjectMapper for " + elementType)); } boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); @@ -138,20 +139,22 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple Flux tokens = Jackson2Tokenizer.tokenize(processed, mapper.getFactory(), mapper, true, forceUseOfBigDecimal, getMaxInMemorySize()); - ObjectReader reader = getObjectReader(mapper, elementType, hints); + ObjectReader objectReader = getObjectReader(mapper, elementType, hints); - return tokens.handle((tokenBuffer, sink) -> { - try { - Object value = reader.readValue(tokenBuffer.asParser(mapper)); - logValue(value, hints); - if (value != null) { - sink.next(value); - } - } - catch (IOException ex) { - sink.error(processException(ex)); - } - }); + return customizeReaderFromStream(objectReader, mimeType, elementType, hints) + .flatMapMany(reader -> tokens.handle((tokenBuffer, sink) -> { + try { + Object value = reader.readValue(tokenBuffer.asParser(mapper)); + logValue(value, hints); + if (value != null) { + sink.next(value); + } + } + catch (IOException ex) { + sink.error(processException(ex)); + } + }) + ); } /** @@ -174,22 +177,37 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple @Override public Mono decodeToMono(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - return DataBufferUtils.join(input, this.maxInMemorySize) - .flatMap(dataBuffer -> Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hints))); + .flatMap(dataBuffer -> { + try { + ObjectReader objectReader = getObjectReader(elementType, mimeType, hints); + return customizeReaderFromStream(objectReader, mimeType, elementType, hints) + .flatMap(reader -> { + try { + return Mono.justOrEmpty(decode(dataBuffer, reader, hints)); + } + catch (DecodingException ex) { + return Mono.error(ex); + } + }); + } + catch (IllegalStateException ex) { + return Mono.error(ex); + } + }); } @Override public Object decode(DataBuffer dataBuffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { + ObjectReader reader = getObjectReader(targetType, mimeType, hints); + reader = customizeReader(reader, mimeType, targetType, hints); + return decode(dataBuffer, reader, hints); + } - ObjectMapper mapper = selectObjectMapper(targetType, mimeType); - if (mapper == null) { - throw new IllegalStateException("No ObjectMapper for " + targetType); - } - + private Object decode(@NonNull DataBuffer dataBuffer, @NonNull ObjectReader objectReader, + @Nullable Map hints) throws DecodingException { try { - ObjectReader objectReader = getObjectReader(mapper, targetType, hints); Object value = objectReader.readValue(dataBuffer.asInputStream()); logValue(value, hints); return value; @@ -202,6 +220,15 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple } } + private ObjectReader getObjectReader(ResolvableType targetType, @Nullable MimeType mimeType, + @Nullable Map hints) { + ObjectMapper mapper = selectObjectMapper(targetType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + targetType); + } + return getObjectReader(mapper, targetType, hints); + } + private ObjectReader getObjectReader( ObjectMapper mapper, ResolvableType elementType, @Nullable Map hints) { @@ -217,6 +244,32 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple mapper.readerFor(javaType); } + /** + * Provides the ability for subclasses to customize the {@link ObjectReader} for deserialization from a stream. + * @param reader the {@link ObjectReader} available for customization + * @param mimeType the MIME type associated with the input stream + * @param elementType the expected type of elements in the output stream + * @param hints additional information about how to do encode + * @return the customized {@link ObjectReader} + */ + protected Mono customizeReaderFromStream(@NonNull ObjectReader reader, @Nullable MimeType mimeType, + ResolvableType elementType, @Nullable Map hints) { + return Mono.just(customizeReader(reader, mimeType, elementType, hints)); + } + + /** + * Provides the ability for subclasses to customize the {@link ObjectReader} for deserialization. + * @param reader the {@link ObjectReader} available for customization + * @param mimeType the MIME type associated with the input stream + * @param elementType the expected type of elements in the output stream + * @param hints additional information about how to do encode + * @return the customized {@link ObjectReader} + */ + protected ObjectReader customizeReader(@NonNull ObjectReader reader, @Nullable MimeType mimeType, + ResolvableType elementType, @Nullable Map hints) { + return reader; + } + @Nullable private Class getContextClass(@Nullable ResolvableType elementType) { MethodParameter param = (elementType != null ? getParameter(elementType) : null); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index d5397f71c69..d55db2b6cf1 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -53,6 +53,7 @@ import org.springframework.http.codec.HttpMessageEncoder; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -86,7 +87,6 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple private final List streamingMediaTypes = new ArrayList<>(1); - /** * Constructor with a Jackson {@link ObjectMapper} to use. */ @@ -150,7 +150,9 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple if (inputStream instanceof Mono) { return Mono.from(inputStream) - .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) + .flatMap(value -> createEncodingToolsForStream(value, elementType, mimeType, hints) + .map(tools -> encodeValue(value, tools.mapper(), tools.writer(), + bufferFactory, mimeType, hints))) .flux(); } @@ -191,6 +193,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple dataBuffer); }) .concatWith(Mono.fromCallable(() -> bufferFactory.wrap(helper.getSuffix()))); + } return dataBufferFlux @@ -213,22 +216,21 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple @Override public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { + ObjectEncodingTools encodingTools = createEncodingTools(value, valueType, mimeType, hints); + ObjectWriter writer = encodingTools.writer(); + writer = customizeWriter(writer, mimeType, valueType, hints); + return encodeValue(value, encodingTools.mapper(), writer, bufferFactory, mimeType, hints); + } + + private DataBuffer encodeValue(Object value, ObjectMapper mapper, ObjectWriter writer, + DataBufferFactory bufferFactory, @Nullable MimeType mimeType, @Nullable Map hints) { - Class jsonView = null; FilterProvider filters = null; if (value instanceof MappingJacksonValue mappingJacksonValue) { value = mappingJacksonValue.getValue(); - valueType = ResolvableType.forInstance(value); - jsonView = mappingJacksonValue.getSerializationView(); filters = mappingJacksonValue.getFilters(); } - ObjectMapper mapper = selectObjectMapper(valueType, mimeType); - if (mapper == null) { - throw new IllegalStateException("No ObjectMapper for " + valueType); - } - - ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints); if (filters != null) { writer = writer.with(filters); } @@ -322,6 +324,35 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple } } + private Mono createEncodingToolsForStream(Object value, ResolvableType valueType, + @Nullable MimeType mimeType, @Nullable Map hints) { + try { + ObjectEncodingTools encodingTools = createEncodingTools(value, valueType, mimeType, hints); + ObjectWriter objectWriter = encodingTools.writer(); + return customizeWriterFromStream(objectWriter, mimeType, valueType, hints) + .map(customizedWriter -> new ObjectEncodingTools(encodingTools.mapper(), customizedWriter)); + } + catch (IllegalStateException ex) { + return Mono.error(ex); + } + } + + private ObjectEncodingTools createEncodingTools(Object value, ResolvableType valueType, + @Nullable MimeType mimeType, @Nullable Map hints) { + Class jsonView = null; + if (value instanceof MappingJacksonValue mappingJacksonValue) { + valueType = ResolvableType.forInstance(mappingJacksonValue.getValue()); + jsonView = mappingJacksonValue.getSerializationView(); + } + + ObjectMapper mapper = selectObjectMapper(valueType, mimeType); + if (mapper == null) { + throw new IllegalStateException("No ObjectMapper for " + valueType); + } + ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints); + return new ObjectEncodingTools(mapper, writer); + } + private ObjectWriter createObjectWriter( ObjectMapper mapper, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Class jsonView, @Nullable Map hints) { @@ -334,12 +365,32 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple if (javaType.isContainerType()) { writer = writer.forType(javaType); } - return customizeWriter(writer, mimeType, valueType, hints); + return writer; } + /** + * Provides the ability for subclasses to customize the {@link ObjectWriter} for serialization from a stream. + * @param writer the {@link ObjectWriter} available for customization + * @param mimeType the MIME type associated with the input stream + * @param elementType the expected type of elements in the output stream + * @param hints additional information about how to do encode + * @return the customized {@link ObjectWriter} + */ + protected Mono customizeWriterFromStream(@NonNull ObjectWriter writer, @Nullable MimeType mimeType, + ResolvableType elementType, @Nullable Map hints) { + return Mono.just(customizeWriter(writer, mimeType, elementType, hints)); + } + + /** + * Provides the ability for subclasses to customize the {@link ObjectWriter} for serialization. + * @param writer the {@link ObjectWriter} available for customization + * @param mimeType the MIME type associated with the input stream + * @param elementType the expected type of elements in the output stream + * @param hints additional information about how to do encode + * @return the customized {@link ObjectWriter} + */ protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType, ResolvableType elementType, @Nullable Map hints) { - return writer; } @@ -438,4 +489,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple } } + + private record ObjectEncodingTools(ObjectMapper mapper, ObjectWriter writer) { + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index bad8a410544..a35ae7c87fd 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -59,6 +59,7 @@ import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -380,6 +381,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener Class deserializationView = mappingJacksonInputMessage.getDeserializationView(); if (deserializationView != null) { ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType); + objectReader = customizeReader(objectReader, javaType); if (isUnicode) { return objectReader.readValue(inputStream); } @@ -389,12 +391,15 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } } } + + ObjectReader objectReader = objectMapper.reader().forType(javaType); + objectReader = customizeReader(objectReader, javaType); if (isUnicode) { - return objectMapper.readValue(inputStream, javaType); + return objectReader.readValue(inputStream); } else { Reader reader = new InputStreamReader(inputStream, charset); - return objectMapper.readValue(reader, javaType); + return objectReader.readValue(reader); } } catch (InvalidDefinitionException ex) { @@ -405,6 +410,16 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } } + /** + * Provides the ability for subclasses to customize the {@link ObjectReader} for deserialization. + * @param reader the {@link ObjectReader} available for customization + * @param javaType the specified type to deserialize to + * @return the customized {@link ObjectReader} + */ + protected ObjectReader customizeReader(@NonNull ObjectReader reader, JavaType javaType) { + return reader; + } + /** * Determine the charset to use for JSON input. *

By default this is either the charset from the input {@code MediaType} @@ -465,6 +480,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { objectWriter = objectWriter.with(this.ssePrettyPrinter); } + objectWriter = customizeWriter(objectWriter, javaType, contentType); objectWriter.writeValue(generator, value); writeSuffix(generator, object); @@ -478,6 +494,18 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener } } + /** + * Provides the ability for subclasses to customize the {@link ObjectWriter} for serialization. + * @param writer the {@link ObjectWriter} available for customization + * @param javaType the specified type to serialize from + * @param contentType the output content type + * @return the customized {@link ObjectWriter} + */ + protected ObjectWriter customizeWriter(@NonNull ObjectWriter writer, @Nullable JavaType javaType, + @Nullable MediaType contentType) { + return writer; + } + /** * Write a prefix before the main content. * @param generator the generator to use for writing content. diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonDecoderTests.java new file mode 100644 index 00000000000..bfd456953be --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonDecoderTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2021 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.http.codec.json; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectReader; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.testfixture.codec.AbstractDecoderTests; +import org.springframework.util.MimeType; + +/** + * Unit tests for a customized {@link Jackson2JsonDecoder}. + * + * @author Jason Laber + */ +public class CustomizedJackson2JsonDecoderTests extends AbstractDecoderTests { + + public CustomizedJackson2JsonDecoderTests() { + super(new Jackson2JsonDecoderWithCustomization()); + } + + @Override + public void canDecode() throws Exception { + // Not Testing, covered under Jackson2JsonDecoderTests + } + + @Override + @Test + public void decode() throws Exception { + Flux input = Flux.concat(stringBuffer("{\"property\":\"Value1\"}")); + + testDecodeAll(input, MyCustomizedDecoderBean.class, step -> step + .expectNextMatches(obj -> obj.getProperty() == MyCustomDecoderEnum.VAL1) + .verifyComplete()); + } + + @Override + @Test + public void decodeToMono() throws Exception { + Mono input = stringBuffer("{\"property\":\"Value2\"}"); + + ResolvableType elementType = ResolvableType.forClass(MyCustomizedDecoderBean.class); + + testDecodeToMono(input, elementType, step -> step + .expectNextMatches(obj -> ((MyCustomizedDecoderBean)obj).getProperty() == MyCustomDecoderEnum.VAL2) + .expectComplete() + .verify(), null, null); + } + + private Mono stringBuffer(String value) { + return stringBuffer(value, StandardCharsets.UTF_8); + } + + private Mono stringBuffer(String value, Charset charset) { + return Mono.defer(() -> { + byte[] bytes = value.getBytes(charset); + DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return Mono.just(buffer); + }); + } + + public static class MyCustomizedDecoderBean { + + private MyCustomDecoderEnum property; + + public MyCustomDecoderEnum getProperty() { + return property; + } + + public void setProperty(MyCustomDecoderEnum property) { + this.property = property; + } + } + + public enum MyCustomDecoderEnum { + VAL1, + VAL2; + + @Override + public String toString() { + return this == VAL1 ? "Value1" : "Value2"; + } + } + + private static class Jackson2JsonDecoderWithCustomization extends Jackson2JsonDecoder { + + @Override + protected Mono customizeReaderFromStream(ObjectReader reader, MimeType mimeType, ResolvableType elementType, Map hints) { + return Mono.just(reader.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING)); + } + + @Override + protected ObjectReader customizeReader(ObjectReader reader, MimeType mimeType, ResolvableType elementType, Map hints) { + return reader.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonEncoderTests.java new file mode 100644 index 00000000000..dcfb6f26630 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/CustomizedJackson2JsonEncoderTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2022 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.http.codec.json; + +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.util.MimeType; + +import static org.springframework.http.MediaType.APPLICATION_NDJSON; + +/** + * Unit tests for a customized {@link Jackson2JsonEncoder}. + * + * @author Jason Laber + */ +public class CustomizedJackson2JsonEncoderTests extends AbstractEncoderTests { + + public CustomizedJackson2JsonEncoderTests() { + super(new Jackson2JsonEncoderWithCustomization()); + } + + + @Override + public void canEncode() throws Exception { + // Not Testing, covered under Jackson2JsonEncoderTests + } + + @Override + @Test + public void encode() throws Exception { + Flux input = Flux.just( + new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL1), + new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL2) + ); + + testEncodeAll(input, ResolvableType.forClass(MyCustomizedEncoderBean.class), APPLICATION_NDJSON, null, step -> step + .consumeNextWith(expectString("{\"property\":\"Value1\"}\n")) + .consumeNextWith(expectString("{\"property\":\"Value2\"}\n")) + .verifyComplete() + ); + } + + @Test + public void encodeNonStream() { + Flux input = Flux.just( + new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL1), + new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL2) + ); + + testEncode(input, MyCustomizedEncoderBean.class, step -> step + .consumeNextWith(expectString("[" + + "{\"property\":\"Value1\"}," + + "{\"property\":\"Value2\"}]") + .andThen(DataBufferUtils::release)) + .verifyComplete()); + } + + public static class MyCustomizedEncoderBean { + + private MyCustomEncoderEnum property; + + public MyCustomizedEncoderBean(MyCustomEncoderEnum property) { + this.property = property; + } + + public MyCustomEncoderEnum getProperty() { + return property; + } + + public void setProperty(MyCustomEncoderEnum property) { + this.property = property; + } + } + + public enum MyCustomEncoderEnum { + VAL1, + VAL2; + + @Override + public String toString() { + return this == VAL1 ? "Value1" : "Value2"; + } + } + + private static class Jackson2JsonEncoderWithCustomization extends Jackson2JsonEncoder { + + @Override + protected Mono customizeWriterFromStream(ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map hints) { + return Mono.just(writer.with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)); + } + + @Override + protected ObjectWriter customizeWriter(ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map hints) { + return writer.with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index 62852876a07..415f910be1a 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -27,8 +27,12 @@ import java.util.Map; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ser.FilterProvider; import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; @@ -528,6 +532,39 @@ public class MappingJackson2HttpMessageConverterTests { assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType); } + @Test + public void readWithCustomized() throws IOException { + MappingJackson2HttpMessageConverterWithCustomization customizedConverter = + new MappingJackson2HttpMessageConverterWithCustomization(); + String body = "{\"property\":\"Value1\"}"; + MockHttpInputMessage inputMessage1 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); + MockHttpInputMessage inputMessage2 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); + inputMessage1.getHeaders().setContentType(new MediaType("application", "json")); + inputMessage2.getHeaders().setContentType(new MediaType("application", "json")); + + assertThatExceptionOfType(HttpMessageNotReadableException.class) + .isThrownBy(() -> converter.read(MyCustomizedBean.class, inputMessage1)); + + MyCustomizedBean customizedResult = (MyCustomizedBean) customizedConverter.read(MyCustomizedBean.class, inputMessage2); + assertThat(customizedResult.getProperty()).isEqualTo(MyCustomEnum.VAL1); + } + + @Test + public void writeWithCustomized() throws IOException { + MappingJackson2HttpMessageConverterWithCustomization customizedConverter = + new MappingJackson2HttpMessageConverterWithCustomization(); + MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); + MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); + MyCustomizedBean body = new MyCustomizedBean(); + body.setProperty(MyCustomEnum.VAL2); + converter.write(body, null, outputMessage1); + customizedConverter.write(body, null, outputMessage2); + String result1 = outputMessage1.getBodyAsString(StandardCharsets.UTF_8); + assertThat(result1.contains("\"property\":\"VAL2\"")).isTrue(); + String result2 = outputMessage2.getBodyAsString(StandardCharsets.UTF_8); + assertThat(result2.contains("\"property\":\"Value2\"")).isTrue(); + } + interface MyInterface { @@ -537,7 +574,7 @@ public class MappingJackson2HttpMessageConverterTests { } - public static class MyBase implements MyInterface{ + public static class MyBase implements MyInterface { private String string; @@ -713,4 +750,40 @@ public class MappingJackson2HttpMessageConverterTests { } } + public static class MyCustomizedBean { + + private MyCustomEnum property; + + public MyCustomEnum getProperty() { + return property; + } + + public void setProperty(MyCustomEnum property) { + this.property = property; + } + } + + public enum MyCustomEnum { + VAL1, + VAL2; + + @Override + public String toString() { + return this == VAL1 ? "Value1" : "Value2"; + } + } + + private static class MappingJackson2HttpMessageConverterWithCustomization extends MappingJackson2HttpMessageConverter { + + @Override + protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) { + return reader.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + } + + @Override + protected ObjectWriter customizeWriter(ObjectWriter writer, JavaType javaType, MediaType contentType) { + return writer.with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + } + } + }