From e93e55b456bbe320d2330fa37a975fe25feada1a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 10 Feb 2026 09:58:25 +0100 Subject: [PATCH] Fix wildcard MIME type support in messaging converters Prior to this commit, the "application/*+json" wildcard MIME type was added to the list of supported MIME types in the JSON messaging converter. This change wasn't fully reflected in the `AbstractMessageConverter`, because only strict matching of type and subtybe were considered. This commit updates the `AbstractMessageConverter` to not only check the type and subtype, but also check whether the supported MIME type includes the one given as a parameter. Fixes gh-36285 --- .../converter/AbstractMessageConverter.java | 2 +- .../JacksonJsonMessageConverterTests.java | 379 ++++++++++++++++++ .../MappingJackson2MessageConverterTests.java | 20 +- 3 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java index 5f6b59ca5af..550220f5b24 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java @@ -236,7 +236,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter return !isStrictContentTypeMatch(); } for (MimeType current : getSupportedMimeTypes()) { - if (current.getType().equals(mimeType.getType()) && current.getSubtype().equals(mimeType.getSubtype())) { + if (current.includes(mimeType)) { return true; } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java new file mode 100644 index 00000000000..de42d6144c1 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java @@ -0,0 +1,379 @@ +/* + * Copyright 2002-present 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.messaging.converter; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonView; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.DeserializationFeature; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.within; + +/** + * Tests for {@link JacksonJsonMessageConverter}. + * + * @author Brian Clozel + * @author Rossen Stoyanchev + * @author Sebastien Deleuze + */ +class JacksonJsonMessageConverterTests { + + @Test + void defaultConstructor() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + assertThat(converter.getSupportedMimeTypes()).contains(new MimeType("application", "json")); + assertThat(converter.getJsonMapper().deserializationConfig() + .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + } + + @Test // SPR-12724 + void mimetypeParametrizedConstructor() { + MimeType mimetype = new MimeType("application", "xml", StandardCharsets.UTF_8); + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(mimetype); + assertThat(converter.getSupportedMimeTypes()).contains(mimetype); + assertThat(converter.getJsonMapper().deserializationConfig() + .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + } + + @Test // SPR-12724 + void mimetypesParametrizedConstructor() { + MimeType jsonMimetype = new MimeType("application", "json", StandardCharsets.UTF_8); + MimeType xmlMimetype = new MimeType("application", "xml", StandardCharsets.UTF_8); + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(jsonMimetype, xmlMimetype); + assertThat(converter.getSupportedMimeTypes()).contains(jsonMimetype, xmlMimetype); + assertThat(converter.getJsonMapper().deserializationConfig() + .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + } + + @Test + void supportJsonMimeType() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + Message message = MessageBuilder.withPayload("foo") + .setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build(); + assertThat(converter.supportsMimeType(message.getHeaders())).isTrue(); + } + + @Test + void supportVendorJsonMimeTypes() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + Message message = MessageBuilder.withPayload("foo") + .setHeader(MessageHeaders.CONTENT_TYPE, "application/vnd.springframework.type+json").build(); + assertThat(converter.supportsMimeType(message.getHeaders())).isTrue(); + } + + @Test + void fromMessage() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + MyBean actual = (MyBean) converter.fromMessage(message, MyBean.class); + + assertThat(actual.getString()).isEqualTo("Foo"); + assertThat(actual.getNumber()).isEqualTo(42); + assertThat(actual.getFraction()).isCloseTo(42F, within(0F)); + assertThat(actual.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); + assertThat(actual.isBool()).isTrue(); + assertThat(actual.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); + } + + @Test + void fromMessageUntyped() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"]," + + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + @SuppressWarnings("unchecked") + HashMap actual = (HashMap) converter.fromMessage(message, HashMap.class); + + assertThat(actual.get("string")).isEqualTo("Foo"); + assertThat(actual.get("number")).isEqualTo(42); + assertThat((Double) actual.get("fraction")).isCloseTo(42D, within(0D)); + assertThat(actual.get("array")).isEqualTo(Arrays.asList("Foo", "Bar")); + assertThat(actual.get("bool")).isEqualTo(Boolean.TRUE); + assertThat(actual.get("bytes")).isEqualTo("AQI="); + } + + @Test // gh-22386 + public void fromMessageMatchingInstance() { + MyBean myBean = new MyBean(); + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + Message message = MessageBuilder.withPayload(myBean).build(); + assertThat(converter.fromMessage(message, MyBean.class)).isSameAs(myBean); + } + + @Test + void fromMessageInvalidJson() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "FooBar"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + assertThatExceptionOfType(MessageConversionException.class).isThrownBy(() -> + converter.fromMessage(message, MyBean.class)); + } + + @Test + void fromMessageValidJsonWithUnknownProperty() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + MyBean myBean = (MyBean)converter.fromMessage(message, MyBean.class); + assertThat(myBean.getString()).isEqualTo("string"); + } + + @Test // SPR-16252 + public void fromMessageToList() throws Exception { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "[1, 2, 3, 4, 5, 6, 7, 8, 9]"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + + Method method = getClass().getDeclaredMethod("handleList", List.class); + MethodParameter param = new MethodParameter(method, 0); + Object actual = converter.fromMessage(message, List.class, param); + + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L)); + } + + @Test // SPR-16486 + public void fromMessageToMessageWithPojo() throws Exception { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + String payload = "{\"string\":\"foo\"}"; + Message message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build(); + + Method method = getClass().getDeclaredMethod("handleMessage", Message.class); + MethodParameter param = new MethodParameter(method, 0); + Object actual = converter.fromMessage(message, MyBean.class, param); + + assertThat(actual).isInstanceOf(MyBean.class); + assertThat(((MyBean) actual).getString()).isEqualTo("foo"); + } + + @Test + void toMessage() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + MyBean payload = new MyBean(); + payload.setString("Foo"); + payload.setNumber(42); + payload.setFraction(42F); + payload.setArray(new String[]{"Foo", "Bar"}); + payload.setBool(true); + payload.setBytes(new byte[]{0x1, 0x2}); + + Message message = converter.toMessage(payload, null); + String actual = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8); + + assertThat(actual).contains("\"string\":\"Foo\""); + assertThat(actual).contains("\"number\":42"); + assertThat(actual).contains("fraction\":42.0"); + assertThat(actual).contains("\"array\":[\"Foo\",\"Bar\"]"); + assertThat(actual).contains("\"bool\":true"); + assertThat(actual).contains("\"bytes\":\"AQI=\""); + assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)).as("Invalid content-type").isEqualTo(new MimeType("application", "json")); + } + + @Test + void toMessageUtf16() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + MimeType contentType = new MimeType("application", "json", StandardCharsets.UTF_16BE); + Map map = new HashMap<>(); + map.put(MessageHeaders.CONTENT_TYPE, contentType); + MessageHeaders headers = new MessageHeaders(map); + String payload = "H\u00e9llo W\u00f6rld"; + Message message = converter.toMessage(payload, headers); + + assertThat(new String((byte[]) message.getPayload(), StandardCharsets.UTF_16BE)).isEqualTo("\"" + payload + "\""); + assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE)).isEqualTo(contentType); + } + + @Test + void toMessageUtf16String() { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + converter.setSerializedPayloadClass(String.class); + + MimeType contentType = new MimeType("application", "json", StandardCharsets.UTF_16BE); + Map map = new HashMap<>(); + map.put(MessageHeaders.CONTENT_TYPE, contentType); + MessageHeaders headers = new MessageHeaders(map); + String payload = "H\u00e9llo W\u00f6rld"; + Message message = converter.toMessage(payload, headers); + + assertThat(message.getPayload()).isEqualTo("\"" + payload + "\""); + assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE)).isEqualTo(contentType); + } + + @Test + void toMessageJsonView() throws Exception { + JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(); + + Map map = new HashMap<>(); + Method method = getClass().getDeclaredMethod("jsonViewResponse"); + MethodParameter returnType = new MethodParameter(method, -1); + Message message = converter.toMessage(jsonViewResponse(), new MessageHeaders(map), returnType); + String actual = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8); + + assertThat(actual).contains("\"withView1\":\"with\""); + assertThat(actual).contains("\"withView2\":\"with\""); + assertThat(actual).doesNotContain("\"withoutView\":\"with\""); + + method = getClass().getDeclaredMethod("jsonViewPayload", JacksonViewBean.class); + MethodParameter param = new MethodParameter(method, 0); + JacksonViewBean back = (JacksonViewBean) converter.fromMessage(message, JacksonViewBean.class, param); + assertThat(back.getWithView1()).isNull(); + assertThat(back.getWithView2()).isEqualTo("with"); + assertThat(back.getWithoutView()).isNull(); + } + + + + @JsonView(MyJacksonView1.class) + public JacksonViewBean jsonViewResponse() { + JacksonViewBean bean = new JacksonViewBean(); + bean.setWithView1("with"); + bean.setWithView2("with"); + bean.setWithoutView("with"); + return bean; + } + + public void jsonViewPayload(@JsonView(MyJacksonView2.class) JacksonViewBean payload) { + } + + void handleList(List payload) { + } + + void handleMessage(Message message) { + } + + + public static class MyBean { + + private String string; + + private int number; + + private float fraction; + + private String[] array; + + private boolean bool; + + private byte[] bytes; + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public float getFraction() { + return fraction; + } + + public void setFraction(float fraction) { + this.fraction = fraction; + } + + public String[] getArray() { + return array; + } + + public void setArray(String[] array) { + this.array = array; + } + } + + + public interface MyJacksonView1 {} + + public interface MyJacksonView2 {} + + + public static class JacksonViewBean { + + @JsonView(MyJacksonView1.class) + private String withView1; + + @JsonView({MyJacksonView1.class, MyJacksonView2.class}) + private String withView2; + + private String withoutView; + + public String getWithView1() { + return withView1; + } + + public void setWithView1(String withView1) { + this.withView1 = withView1; + } + + public String getWithView2() { + return withView2; + } + + public void setWithView2(String withView2) { + this.withView2 = withView2; + } + + public String getWithoutView() { + return withoutView; + } + + public void setWithoutView(String withoutView) { + this.withoutView = withoutView; + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java index 46214d17bc5..1325e958f4b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java @@ -55,7 +55,7 @@ class MappingJackson2MessageConverterTests { } @Test // SPR-12724 - public void mimetypeParametrizedConstructor() { + void mimetypeParametrizedConstructor() { MimeType mimetype = new MimeType("application", "xml", StandardCharsets.UTF_8); MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(mimetype); assertThat(converter.getSupportedMimeTypes()).contains(mimetype); @@ -64,7 +64,7 @@ class MappingJackson2MessageConverterTests { } @Test // SPR-12724 - public void mimetypesParametrizedConstructor() { + void mimetypesParametrizedConstructor() { MimeType jsonMimetype = new MimeType("application", "json", StandardCharsets.UTF_8); MimeType xmlMimetype = new MimeType("application", "xml", StandardCharsets.UTF_8); MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(jsonMimetype, xmlMimetype); @@ -73,6 +73,22 @@ class MappingJackson2MessageConverterTests { .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); } + @Test + void supportJsonMimeType() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + Message message = MessageBuilder.withPayload("foo") + .setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build(); + assertThat(converter.supportsMimeType(message.getHeaders())).isTrue(); + } + + @Test + void supportVendorJsonMimeTypes() { + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); + Message message = MessageBuilder.withPayload("foo") + .setHeader(MessageHeaders.CONTENT_TYPE, "application/vnd.springframework.type+json").build(); + assertThat(converter.supportsMimeType(message.getHeaders())).isTrue(); + } + @Test void fromMessage() { MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();