Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add web support for YAML via Jackson #32345

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
@@ -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 @@ -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