Skip to content

Commit

Permalink
Add web support for Yaml via Jackson
Browse files Browse the repository at this point in the history
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 spring-projectsgh-32345
  • Loading branch information
Hyoungjune authored and sdeleuze committed Mar 11, 2024
1 parent 246e497 commit 6a8f0d6
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 2 deletions.
1 change: 1 addition & 0 deletions spring-web/spring-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies {
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
optional("com.fasterxml.woodstox:woodstox-core")
optional("com.google.code.gson:gson")
optional("com.google.protobuf:protobuf-java-util")
Expand Down
14 changes: 13 additions & 1 deletion spring-web/src/main/java/org/springframework/http/MediaType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand Down Expand Up @@ -45,6 +45,7 @@
* @author Sebastien Deleuze
* @author Kazuki Shimizu
* @author Sam Brannen
* @author Hyoungjune Kim
* @since 3.0
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.1">
* HTTP 1.1: Semantics and Content, section 3.1.1.1</a>
Expand Down Expand Up @@ -311,6 +312,16 @@ public class MediaType extends MimeType implements Serializable {
*/
public static final String APPLICATION_XML_VALUE = "application/xml";

/**
* Public constant media type for {@code application/yaml}.
*/
public static final MediaType APPLICATION_YAML;

/**
* A String equivalent of {@link MediaType#APPLICATION_YAML}.
*/
public static final String APPLICATION_YAML_VALE = "application/yaml";

/**
* Public constant media type for {@code image/gif}.
*/
Expand Down Expand Up @@ -454,6 +465,7 @@ public class MediaType extends MimeType implements Serializable {
APPLICATION_STREAM_JSON = new MediaType("application", "stream+json");
APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml");
APPLICATION_XML = new MediaType("application", "xml");
APPLICATION_YAML = new MediaType("application", "yaml");
IMAGE_GIF = new MediaType("image", "gif");
IMAGE_JPEG = new MediaType("image", "jpeg");
IMAGE_PNG = new MediaType("image", "png");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationContext;
Expand Down Expand Up @@ -95,6 +96,7 @@
* @author Juergen Hoeller
* @author Tadaya Tsuyukubo
* @author Eddú Meléndez
* @author Hyoungjune Kim
* @since 4.1.1
* @see #build()
* @see #configure(ObjectMapper)
Expand Down Expand Up @@ -936,6 +938,15 @@ public static Jackson2ObjectMapperBuilder cbor() {
return new Jackson2ObjectMapperBuilder().factory(new CborFactoryInitializer().create());
}

/**
* Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to
* build a Yaml data format {@link ObjectMapper} instance.
* @since 6.2
*/
public static Jackson2ObjectMapperBuilder yaml() {
return new Jackson2ObjectMapperBuilder().factory(new YamlFactoryInitializer().create());
}


private static class XmlObjectMapperInitializer {

Expand Down Expand Up @@ -976,4 +987,11 @@ public JsonFactory create() {
}
}

private static class YamlFactoryInitializer {

public JsonFactory create() {
return new YAMLFactory();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2002-2024 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.converter.yaml;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.Assert;

/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter
* HttpMessageConverter} that can read and write the <a href="https://yaml.io/">YAML</a>
* data format using <a href="https://github.com/FasterXML/jackson-dataformat-yaml/tree/master">
* the dedicated Jackson 2.x extension</a>.
*
* <p>By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALE}
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes
* supportedMediaTypes} property.
*
* <p>The default constructor uses the default configuration provided by
* {@link Jackson2ObjectMapperBuilder}.
*
* @author Hyoungjune Kim
* @since 6.2
*/
public class MappingJackson2YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {

/**
* Construct a new {@code MappingJackson2YamlHttpMessageConverter} using the
* default configuration provided by {@code Jackson2ObjectMapperBuilder}.
*/
public MappingJackson2YamlHttpMessageConverter() {
this(Jackson2ObjectMapperBuilder.yaml().build());
}

/**
* Construct a new {@code MappingJackson2YamlHttpMessageConverter} with a
* custom {@link ObjectMapper} (must be configured with a {@code YAMLFactory}
* instance).
* <p>You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
* @see Jackson2ObjectMapperBuilder#yaml()
*/
public MappingJackson2YamlHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_YAML);
Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
}


/**
* {@inheritDoc}
* The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance.
*/
@Override
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
super.setObjectMapper(objectMapper);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Provides an HttpMessageConverter for the Yaml data format.
*/
@NonNullApi
@NonNullFields
package org.springframework.http.converter.yaml;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
Expand All @@ -60,6 +61,7 @@
* Default implementation of {@link RestClient.Builder}.
*
* @author Arjen Poutsma
* @author Hyoungjune Kim
* @since 6.1
*/
final class DefaultRestClientBuilder implements RestClient.Builder {
Expand All @@ -86,6 +88,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {

private static final boolean jackson2CborPresent;

private static final boolean jackson2YamlPresent;


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

@Nullable
Expand Down Expand Up @@ -394,6 +399,9 @@ else if (jsonbPresent) {
if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
if (jackson2YamlPresent) {
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
}
}
return this.messageConverters;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
Expand Down Expand Up @@ -108,6 +109,7 @@
* @author Juergen Hoeller
* @author Sam Brannen
* @author Sebastien Deleuze
* @author Hyoungjune Kim
* @since 3.0
* @see HttpMessageConverter
* @see RequestCallback
Expand All @@ -128,6 +130,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat

private static final boolean jackson2CborPresent;

private static final boolean jackson2YamlPresent;

private static final boolean gsonPresent;

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

if (jackson2YamlPresent) {
this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
}

updateErrorHandlerConverters();
this.uriTemplateHandler = initUriTemplateHandler();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import kotlin.ranges.IntRange;
import org.junit.jupiter.api.Test;
Expand All @@ -95,6 +96,7 @@
*
* @author Sebastien Deleuze
* @author Eddú Meléndez
* @author Hyoungjune Kim
*/
@SuppressWarnings("deprecation")
class Jackson2ObjectMapperBuilderTests {
Expand Down Expand Up @@ -588,6 +590,13 @@ void factory() {
assertThat(objectMapper.getFactory().getClass()).isEqualTo(SmileFactory.class);
}

@Test
void yaml() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.yaml().build();
assertThat(objectMapper).isNotNull();
assertThat(objectMapper.getFactory().getClass()).isEqualTo(YAMLFactory.class);
}

@Test
void visibility() throws JsonProcessingException {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
Expand Down
1 change: 1 addition & 0 deletions spring-webmvc/spring-webmvc.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
optional("com.github.librepdf:openpdf")
optional("com.rometools:rome")
optional("io.micrometer:context-propagation")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -21,6 +21,7 @@

import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.w3c.dom.Element;

import org.springframework.beans.factory.FactoryBean;
Expand Down Expand Up @@ -55,6 +56,7 @@
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
Expand Down Expand Up @@ -148,6 +150,7 @@
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Agim Emruli
* @author Hyoungjune Kim
* @since 3.0
*/
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
Expand All @@ -173,6 +176,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {

private static final boolean jackson2CborPresent;

private static final boolean jackson2YamlPresent;

private static final boolean gsonPresent;

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

Expand Down Expand Up @@ -463,6 +469,9 @@ private Properties getDefaultMediaTypes() {
if (jackson2CborPresent) {
defaultMediaTypes.put("cbor", MediaType.APPLICATION_CBOR_VALUE);
}
if (jackson2YamlPresent) {
defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALE);
}
return defaultMediaTypes;
}

Expand Down Expand Up @@ -614,6 +623,14 @@ else if (gsonPresent) {
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
}
if(jackson2YamlPresent) {
Class<?> type = MappingJackson2YamlHttpMessageConverter.class;
RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
jacksonFactoryDef.getPropertyValues().add("factory", new RootBeanDefinition(YAMLFactory.class));
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
}
}
return messageConverters;
}
Expand Down

0 comments on commit 6a8f0d6

Please sign in to comment.