Skip to content

Commit 6a8f0d6

Browse files
Hyoungjunesdeleuze
Hyoungjune
authored andcommitted
Add web support for Yaml via Jackson
This commit adds support for application/yaml in MediaType and leverages jackson-dataformat-yaml in order to support Yaml in RestTemplate, RestClient and Spring MVC. See gh-32345
1 parent 246e497 commit 6a8f0d6

File tree

11 files changed

+177
-2
lines changed

11 files changed

+177
-2
lines changed

Diff for: spring-web/spring-web.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies {
1515
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
1616
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile")
1717
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
18+
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
1819
optional("com.fasterxml.woodstox:woodstox-core")
1920
optional("com.google.code.gson:gson")
2021
optional("com.google.protobuf:protobuf-java-util")

Diff for: spring-web/src/main/java/org/springframework/http/MediaType.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,6 +45,7 @@
4545
* @author Sebastien Deleuze
4646
* @author Kazuki Shimizu
4747
* @author Sam Brannen
48+
* @author Hyoungjune Kim
4849
* @since 3.0
4950
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1">
5051
* HTTP 1.1: Semantics and Content, section 3.1.1.1</a>
@@ -311,6 +312,16 @@ public class MediaType extends MimeType implements Serializable {
311312
*/
312313
public static final String APPLICATION_XML_VALUE = "application/xml";
313314

315+
/**
316+
* Public constant media type for {@code application/yaml}.
317+
*/
318+
public static final MediaType APPLICATION_YAML;
319+
320+
/**
321+
* A String equivalent of {@link MediaType#APPLICATION_YAML}.
322+
*/
323+
public static final String APPLICATION_YAML_VALE = "application/yaml";
324+
314325
/**
315326
* Public constant media type for {@code image/gif}.
316327
*/
@@ -454,6 +465,7 @@ public class MediaType extends MimeType implements Serializable {
454465
APPLICATION_STREAM_JSON = new MediaType("application", "stream+json");
455466
APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml");
456467
APPLICATION_XML = new MediaType("application", "xml");
468+
APPLICATION_YAML = new MediaType("application", "yaml");
457469
IMAGE_GIF = new MediaType("image", "gif");
458470
IMAGE_JPEG = new MediaType("image", "jpeg");
459471
IMAGE_PNG = new MediaType("image", "png");

Diff for: spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java

+18
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
5757
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
5858
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
59+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
5960

6061
import org.springframework.beans.BeanUtils;
6162
import org.springframework.context.ApplicationContext;
@@ -95,6 +96,7 @@
9596
* @author Juergen Hoeller
9697
* @author Tadaya Tsuyukubo
9798
* @author Eddú Meléndez
99+
* @author Hyoungjune Kim
98100
* @since 4.1.1
99101
* @see #build()
100102
* @see #configure(ObjectMapper)
@@ -936,6 +938,15 @@ public static Jackson2ObjectMapperBuilder cbor() {
936938
return new Jackson2ObjectMapperBuilder().factory(new CborFactoryInitializer().create());
937939
}
938940

941+
/**
942+
* Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to
943+
* build a Yaml data format {@link ObjectMapper} instance.
944+
* @since 6.2
945+
*/
946+
public static Jackson2ObjectMapperBuilder yaml() {
947+
return new Jackson2ObjectMapperBuilder().factory(new YamlFactoryInitializer().create());
948+
}
949+
939950

940951
private static class XmlObjectMapperInitializer {
941952

@@ -976,4 +987,11 @@ public JsonFactory create() {
976987
}
977988
}
978989

990+
private static class YamlFactoryInitializer {
991+
992+
public JsonFactory create() {
993+
return new YAMLFactory();
994+
}
995+
}
996+
979997
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter.yaml;
18+
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
21+
22+
import org.springframework.http.MediaType;
23+
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
24+
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter
29+
* HttpMessageConverter} that can read and write the <a href="https://yaml.io/">YAML</a>
30+
* data format using <a href="https://github.com/FasterXML/jackson-dataformat-yaml/tree/master">
31+
* the dedicated Jackson 2.x extension</a>.
32+
*
33+
* <p>By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALE}
34+
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes
35+
* supportedMediaTypes} property.
36+
*
37+
* <p>The default constructor uses the default configuration provided by
38+
* {@link Jackson2ObjectMapperBuilder}.
39+
*
40+
* @author Hyoungjune Kim
41+
* @since 6.2
42+
*/
43+
public class MappingJackson2YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
44+
45+
/**
46+
* Construct a new {@code MappingJackson2YamlHttpMessageConverter} using the
47+
* default configuration provided by {@code Jackson2ObjectMapperBuilder}.
48+
*/
49+
public MappingJackson2YamlHttpMessageConverter() {
50+
this(Jackson2ObjectMapperBuilder.yaml().build());
51+
}
52+
53+
/**
54+
* Construct a new {@code MappingJackson2YamlHttpMessageConverter} with a
55+
* custom {@link ObjectMapper} (must be configured with a {@code YAMLFactory}
56+
* instance).
57+
* <p>You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
58+
* @see Jackson2ObjectMapperBuilder#yaml()
59+
*/
60+
public MappingJackson2YamlHttpMessageConverter(ObjectMapper objectMapper) {
61+
super(objectMapper, MediaType.APPLICATION_YAML);
62+
Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
63+
}
64+
65+
66+
/**
67+
* {@inheritDoc}
68+
* The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance.
69+
*/
70+
@Override
71+
public void setObjectMapper(ObjectMapper objectMapper) {
72+
Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
73+
super.setObjectMapper(objectMapper);
74+
}
75+
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Provides an HttpMessageConverter for the Yaml data format.
3+
*/
4+
@NonNullApi
5+
@NonNullFields
6+
package org.springframework.http.converter.yaml;
7+
8+
import org.springframework.lang.NonNullApi;
9+
import org.springframework.lang.NonNullFields;

Diff for: spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
4949
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
5050
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
51+
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
5152
import org.springframework.lang.Nullable;
5253
import org.springframework.util.Assert;
5354
import org.springframework.util.ClassUtils;
@@ -60,6 +61,7 @@
6061
* Default implementation of {@link RestClient.Builder}.
6162
*
6263
* @author Arjen Poutsma
64+
* @author Hyoungjune Kim
6365
* @since 6.1
6466
*/
6567
final class DefaultRestClientBuilder implements RestClient.Builder {
@@ -86,6 +88,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
8688

8789
private static final boolean jackson2CborPresent;
8890

91+
private static final boolean jackson2YamlPresent;
92+
8993

9094
static {
9195
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
@@ -101,6 +105,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
101105
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", loader);
102106
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
103107
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
108+
jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", loader);
104109
}
105110

106111
@Nullable
@@ -394,6 +399,9 @@ else if (jsonbPresent) {
394399
if (jackson2CborPresent) {
395400
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
396401
}
402+
if (jackson2YamlPresent) {
403+
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
404+
}
397405
}
398406
return this.messageConverters;
399407
}

Diff for: spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

+9
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
6767
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
6868
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
69+
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
6970
import org.springframework.lang.Nullable;
7071
import org.springframework.util.Assert;
7172
import org.springframework.util.ClassUtils;
@@ -108,6 +109,7 @@
108109
* @author Juergen Hoeller
109110
* @author Sam Brannen
110111
* @author Sebastien Deleuze
112+
* @author Hyoungjune Kim
111113
* @since 3.0
112114
* @see HttpMessageConverter
113115
* @see RequestCallback
@@ -128,6 +130,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
128130

129131
private static final boolean jackson2CborPresent;
130132

133+
private static final boolean jackson2YamlPresent;
134+
131135
private static final boolean gsonPresent;
132136

133137
private static final boolean jsonbPresent;
@@ -149,6 +153,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
149153
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
150154
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
151155
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
156+
jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
152157
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
153158
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
154159
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -222,6 +227,10 @@ else if (kotlinSerializationCborPresent) {
222227
this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
223228
}
224229

230+
if (jackson2YamlPresent) {
231+
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
232+
}
233+
225234
updateErrorHandlerConverters();
226235
this.uriTemplateHandler = initUriTemplateHandler();
227236
}

Diff for: spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java

+9
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
8080
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
8181
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
82+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
8283
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
8384
import kotlin.ranges.IntRange;
8485
import org.junit.jupiter.api.Test;
@@ -95,6 +96,7 @@
9596
*
9697
* @author Sebastien Deleuze
9798
* @author Eddú Meléndez
99+
* @author Hyoungjune Kim
98100
*/
99101
@SuppressWarnings("deprecation")
100102
class Jackson2ObjectMapperBuilderTests {
@@ -588,6 +590,13 @@ void factory() {
588590
assertThat(objectMapper.getFactory().getClass()).isEqualTo(SmileFactory.class);
589591
}
590592

593+
@Test
594+
void yaml() {
595+
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.yaml().build();
596+
assertThat(objectMapper).isNotNull();
597+
assertThat(objectMapper.getFactory().getClass()).isEqualTo(YAMLFactory.class);
598+
}
599+
591600
@Test
592601
void visibility() throws JsonProcessingException {
593602
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()

Diff for: spring-webmvc/spring-webmvc.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies {
1919
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
2020
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile")
2121
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
22+
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
2223
optional("com.github.librepdf:openpdf")
2324
optional("com.rometools:rome")
2425
optional("io.micrometer:context-propagation")

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121

2222
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
2323
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
24+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
2425
import org.w3c.dom.Element;
2526

2627
import org.springframework.beans.factory.FactoryBean;
@@ -55,6 +56,7 @@
5556
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
5657
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
5758
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
59+
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
5860
import org.springframework.lang.Nullable;
5961
import org.springframework.util.Assert;
6062
import org.springframework.util.ClassUtils;
@@ -148,6 +150,7 @@
148150
* @author Rossen Stoyanchev
149151
* @author Brian Clozel
150152
* @author Agim Emruli
153+
* @author Hyoungjune Kim
151154
* @since 3.0
152155
*/
153156
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
@@ -173,6 +176,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
173176

174177
private static final boolean jackson2CborPresent;
175178

179+
private static final boolean jackson2YamlPresent;
180+
176181
private static final boolean gsonPresent;
177182

178183
static {
@@ -185,6 +190,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
185190
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
186191
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
187192
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
193+
jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
188194
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
189195
}
190196

@@ -463,6 +469,9 @@ private Properties getDefaultMediaTypes() {
463469
if (jackson2CborPresent) {
464470
defaultMediaTypes.put("cbor", MediaType.APPLICATION_CBOR_VALUE);
465471
}
472+
if (jackson2YamlPresent) {
473+
defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALE);
474+
}
466475
return defaultMediaTypes;
467476
}
468477

@@ -614,6 +623,14 @@ else if (gsonPresent) {
614623
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
615624
messageConverters.add(jacksonConverterDef);
616625
}
626+
if(jackson2YamlPresent) {
627+
Class<?> type = MappingJackson2YamlHttpMessageConverter.class;
628+
RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
629+
GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
630+
jacksonFactoryDef.getPropertyValues().add("factory", new RootBeanDefinition(YAMLFactory.class));
631+
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
632+
messageConverters.add(jacksonConverterDef);
633+
}
617634
}
618635
return messageConverters;
619636
}

0 commit comments

Comments
 (0)