Allow GSON customization via properties or beans

Update GSON support to allow customization with either properties or
customize beans.

See gh-11498
pull/11536/merge
ioann 7 years ago committed by Phillip Webb
parent 9cb5f3da89
commit ba552f1d24

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2014 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.
@ -16,28 +16,145 @@
package org.springframework.boot.autoconfigure.gson;
import java.util.List;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.LongSerializationPolicy;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Gson.
*
* @author David Liu
* @author Ivan Golovko
* @since 1.2.0
*/
@Configuration
@ConditionalOnClass(Gson.class)
public class GsonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Gson gson() {
return new Gson();
@Configuration
static class GsonConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(Gson.class)
public Gson gson(GsonBuilder gsonBuilder) {
return gsonBuilder.create();
}
}
@Configuration
static class GsonBuilderConfiguration {
@Bean
public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) {
final GsonBuilder gsonBuilder = new GsonBuilder();
customizers.forEach(c -> c.customize(gsonBuilder));
return gsonBuilder;
}
}
@Configuration
@EnableConfigurationProperties(GsonProperties.class)
static class GsonBuilderCustomizerConfiguration {
@Bean
public StandardGsonBuilderCustomizer standardGsonBuilderCustomizer(
GsonProperties gsonProperties) {
return new StandardGsonBuilderCustomizer(gsonProperties);
}
private static final class StandardGsonBuilderCustomizer
implements GsonBuilderCustomizer, Ordered {
private final GsonProperties properties;
StandardGsonBuilderCustomizer(GsonProperties properties) {
this.properties = properties;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(GsonBuilder gsonBuilder) {
boolean generateNonExecutableJson = this.properties
.isGenerateNonExecutableJson();
if (generateNonExecutableJson) {
gsonBuilder.generateNonExecutableJson();
}
boolean excludeFieldsWithoutExposeAnnotation = this.properties
.isExcludeFieldsWithoutExposeAnnotation();
if (excludeFieldsWithoutExposeAnnotation) {
gsonBuilder.excludeFieldsWithoutExposeAnnotation();
}
boolean serializeNulls = this.properties.isSerializeNulls();
if (serializeNulls) {
gsonBuilder.serializeNulls();
}
boolean enableComplexMapKeySerialization = this.properties
.isEnableComplexMapKeySerialization();
if (enableComplexMapKeySerialization) {
gsonBuilder.enableComplexMapKeySerialization();
}
boolean disableInnerClassSerialization = this.properties
.isDisableInnerClassSerialization();
if (disableInnerClassSerialization) {
gsonBuilder.disableInnerClassSerialization();
}
LongSerializationPolicy longSerializationPolicy = this.properties
.getLongSerializationPolicy();
if (longSerializationPolicy != null) {
gsonBuilder.setLongSerializationPolicy(longSerializationPolicy);
}
FieldNamingPolicy fieldNamingPolicy = this.properties
.getFieldNamingPolicy();
if (fieldNamingPolicy != null) {
gsonBuilder.setFieldNamingPolicy(fieldNamingPolicy);
}
boolean prettyPrinting = this.properties.isPrettyPrinting();
if (prettyPrinting) {
gsonBuilder.setPrettyPrinting();
}
boolean isLenient = this.properties.isLenient();
if (isLenient) {
gsonBuilder.setLenient();
}
boolean disableHtmlEscaping = this.properties.isDisableHtmlEscaping();
if (disableHtmlEscaping) {
gsonBuilder.disableHtmlEscaping();
}
String dateFormat = this.properties.getDateFormat();
if (dateFormat != null) {
gsonBuilder.setDateFormat(dateFormat);
}
}
}
}
}

@ -0,0 +1,37 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.gson;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link Gson} via {@link GsonBuilder} retaining its default auto-configuration.
*
* @author Ivan Golovko
* @since 2.0.0
*/
@FunctionalInterface
public interface GsonBuilderCustomizer {
/**
* Customize the GsonBuilder.
* @param gsonBuilder the GsonBuilder to customize
*/
void customize(GsonBuilder gsonBuilder);
}

@ -0,0 +1,190 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.autoconfigure.gson;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.LongSerializationPolicy;
import com.google.gson.annotations.Expose;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties to configure {@link Gson}.
*
* @author Ivan Golovko
* @since 2.0.0
*/
@ConfigurationProperties(prefix = "spring.gson")
public class GsonProperties {
/**
* Makes the output JSON non-executable in Javascript by prefixing the generated JSON
* with some special text.
*/
private boolean generateNonExecutableJson;
/**
* Configures {@link Gson} to exclude all fields from consideration for serialization
* or deserialization that do not have the {@link Expose} annotation.
*/
private boolean excludeFieldsWithoutExposeAnnotation;
/**
* Configure {@link Gson} to serialize null fields.
*/
private boolean serializeNulls;
/**
* Enabling this feature will only change the serialized form if the map key is a
* complex type (i.e. non-primitive) in its serialized JSON form
*/
private boolean enableComplexMapKeySerialization;
/**
* Configures {@link Gson} to exclude inner classes during serialization.
*/
private boolean disableInnerClassSerialization;
/**
* Configures {@link Gson} to apply a specific serialization policy for Long and long
* objects.
*/
private LongSerializationPolicy longSerializationPolicy;
/**
* Configures {@link Gson} to apply a specific naming policy to an object's field
* during serialization and deserialization.
*/
private FieldNamingPolicy fieldNamingPolicy;
/**
* Configures {@link Gson} to output Json that fits in a page for pretty printing.
* This option only affects Json serialization.
*/
private boolean prettyPrinting;
/**
* By default, {@link Gson} is strict and only accepts JSON as specified by RFC 4627.
* This option makes the parser liberal in what it accepts.
*/
private boolean lenient;
/**
* By default, {@link Gson} escapes HTML characters such as < > etc. Use this option
* to configure Gson to pass-through HTML characters as is.
*/
private boolean disableHtmlEscaping;
/**
* Configures {@link Gson} to serialize Date objects according to the pattern
* provided.
*/
private String dateFormat;
public boolean isGenerateNonExecutableJson() {
return this.generateNonExecutableJson;
}
public void setGenerateNonExecutableJson(boolean generateNonExecutableJson) {
this.generateNonExecutableJson = generateNonExecutableJson;
}
public boolean isExcludeFieldsWithoutExposeAnnotation() {
return this.excludeFieldsWithoutExposeAnnotation;
}
public void setExcludeFieldsWithoutExposeAnnotation(
boolean excludeFieldsWithoutExposeAnnotation) {
this.excludeFieldsWithoutExposeAnnotation = excludeFieldsWithoutExposeAnnotation;
}
public boolean isSerializeNulls() {
return this.serializeNulls;
}
public void setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
}
public boolean isEnableComplexMapKeySerialization() {
return this.enableComplexMapKeySerialization;
}
public void setEnableComplexMapKeySerialization(
boolean enableComplexMapKeySerialization) {
this.enableComplexMapKeySerialization = enableComplexMapKeySerialization;
}
public boolean isDisableInnerClassSerialization() {
return this.disableInnerClassSerialization;
}
public void setDisableInnerClassSerialization(
boolean disableInnerClassSerialization) {
this.disableInnerClassSerialization = disableInnerClassSerialization;
}
public LongSerializationPolicy getLongSerializationPolicy() {
return this.longSerializationPolicy;
}
public void setLongSerializationPolicy(
LongSerializationPolicy longSerializationPolicy) {
this.longSerializationPolicy = longSerializationPolicy;
}
public FieldNamingPolicy getFieldNamingPolicy() {
return this.fieldNamingPolicy;
}
public void setFieldNamingPolicy(FieldNamingPolicy fieldNamingPolicy) {
this.fieldNamingPolicy = fieldNamingPolicy;
}
public boolean isPrettyPrinting() {
return this.prettyPrinting;
}
public void setPrettyPrinting(boolean prettyPrinting) {
this.prettyPrinting = prettyPrinting;
}
public boolean isLenient() {
return this.lenient;
}
public void setLenient(boolean lenient) {
this.lenient = lenient;
}
public boolean isDisableHtmlEscaping() {
return this.disableHtmlEscaping;
}
public void setDisableHtmlEscaping(boolean disableHtmlEscaping) {
this.disableHtmlEscaping = disableHtmlEscaping;
}
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2016 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.
@ -16,49 +16,254 @@
package org.springframework.boot.autoconfigure.gson;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import org.junit.After;
import org.junit.Before;
import com.google.gson.LongSerializationPolicy;
import org.joda.time.DateTime;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GsonAutoConfiguration}.
*
* @author David Liu
* @author Ivan Golovko
*/
public class GsonAutoConfigurationTests {
AnnotationConfigApplicationContext context;
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class));
@Before
public void setUp() {
this.context = new AnnotationConfigApplicationContext();
@Test
public void gsonRegistration() {
this.contextRunner.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":1}");
});
}
@After
public void tearDown() {
if (this.context != null) {
this.context.close();
}
@Test
public void generateNonExecutableJson() {
this.contextRunner
.withPropertyValues("spring.gson.generate-non-executable-json:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject()))
.isNotEqualTo("{\"data\":1}");
assertThat(gson.toJson(new DataObject())).endsWith("{\"data\":1}");
});
}
@Test
public void gsonRegistration() {
this.context.register(GsonAutoConfiguration.class);
this.context.refresh();
Gson gson = this.context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{\"data\":\"hello\"}");
public void excludeFieldsWithoutExposeAnnotation() {
this.contextRunner
.withPropertyValues(
"spring.gson.exclude-fields-without-expose-annotation:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{}");
});
}
@Test
public void serializeNulls() {
this.contextRunner.withPropertyValues("spring.gson.serialize-nulls:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.serializeNulls()).isTrue();
});
}
@Test
public void enableComplexMapKeySerialization() {
this.contextRunner
.withPropertyValues(
"spring.gson.enable-complex-map-key-serialization:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
Map<DataObject, String> original = new LinkedHashMap<>();
original.put(new DataObject(), "a");
assertThat(gson.toJson(original)).isEqualTo("[[{\"data\":1},\"a\"]]");
});
}
@Test
public void notDisableInnerClassSerialization() {
this.contextRunner.run(context -> {
Gson gson = context.getBean(Gson.class);
WrapperObject wrapperObject = new WrapperObject();
assertThat(gson.toJson(wrapperObject.new NestedObject()))
.isEqualTo("{\"data\":\"nested\"}");
});
}
@Test
public void disableInnerClassSerialization() {
this.contextRunner
.withPropertyValues("spring.gson.disable-inner-class-serialization:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
WrapperObject wrapperObject = new WrapperObject();
assertThat(gson.toJson(wrapperObject.new NestedObject()))
.isEqualTo("null");
});
}
@Test
public void withLongSerializationPolicy() {
this.contextRunner.withPropertyValues(
"spring.gson.long-serialization-policy:" + LongSerializationPolicy.STRING)
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject()))
.isEqualTo("{\"data\":\"1\"}");
});
}
@Test
public void withFieldNamingPolicy() {
FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.UPPER_CAMEL_CASE;
this.contextRunner
.withPropertyValues(
"spring.gson.field-naming-policy:" + fieldNamingPolicy)
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.fieldNamingStrategy()).isEqualTo(fieldNamingPolicy);
});
}
@Test
public void additionalGsonBuilderCustomization() {
this.contextRunner.withUserConfiguration(GsonBuilderCustomConfig.class)
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject())).isEqualTo("{}");
});
}
@Test
public void withPrettyPrinting() {
this.contextRunner.withPropertyValues("spring.gson.pretty-printing:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.toJson(new DataObject()))
.isEqualTo("{\n \"data\": 1\n}");
});
}
@Test
public void withoutLenient() throws Exception {
this.contextRunner.run(context -> {
Gson gson = context.getBean(Gson.class);
/*
* It seems, that lenient setting not work in version 2.8.2 We get access to
* it via reflection
*/
Field lenientField = gson.getClass().getDeclaredField("lenient");
lenientField.setAccessible(true);
boolean lenient = lenientField.getBoolean(gson);
assertThat(lenient).isFalse();
});
}
@Test
public void withLenient() throws Exception {
this.contextRunner.withPropertyValues("spring.gson.lenient:true").run(context -> {
Gson gson = context.getBean(Gson.class);
/*
* It seems, that lenient setting not work in version 2.8.0 of gson We get
* access to it via reflection
*/
Field lenientField = gson.getClass().getDeclaredField("lenient");
lenientField.setAccessible(true);
boolean lenient = lenientField.getBoolean(gson);
assertThat(lenient).isTrue();
});
}
@Test
public void withHtmlEscaping() {
this.contextRunner.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.htmlSafe()).isTrue();
});
}
@Test
public void withoutHtmlEscaping() {
this.contextRunner.withPropertyValues("spring.gson.disable-html-escaping:true")
.run(context -> {
Gson gson = context.getBean(Gson.class);
assertThat(gson.htmlSafe()).isFalse();
});
}
@Test
public void customDateFormat() {
this.contextRunner.withPropertyValues("spring.gson.date-format:H")
.run(context -> {
Gson gson = context.getBean(Gson.class);
DateTime dateTime = new DateTime(1988, 6, 25, 20, 30);
Date date = dateTime.toDate();
assertThat(gson.toJson(date)).isEqualTo("\"20\"");
});
}
protected static class GsonBuilderCustomConfig {
@Bean
public GsonBuilderCustomizer customSerializationExclusionStrategy() {
return (gsonBuilder) -> gsonBuilder
.addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
return "data".equals(fieldAttributes.getName());
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
});
}
}
public class DataObject {
@SuppressWarnings("unused")
private String data = "hello";
public static final String STATIC_DATA = "bye";
@SuppressWarnings("unused")
private Long data = 1L;
public void setData(Long data) {
this.data = data;
}
}
public class WrapperObject {
@SuppressWarnings("unused")
class NestedObject {
@SuppressWarnings("unused")
private String data = "nested";
}
}
}

@ -340,6 +340,19 @@ content into your application. Rather, pick only the properties that you need.
spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized.
spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10".
# GSON ({sc-spring-boot-autoconfigure}/gson/GsonProperties.{sc-ext}[GsonProperties])
spring.gson.date-format= # Configures Gson to serialize Date objects according to the pattern provided.
spring.gson.disable-html-escaping=false # By default, Gson escapes HTML characters such as < > etc. Use this option to configure Gson to pass-through HTML characters as is.
spring.gson.disable-inner-class-serialization=false # Configures Gson to exclude inner classes during serialization.
spring.gson.enable-complex-map-key-serialization=false # Enabling this feature will only change the serialized form if the map key is a complex type (i.e. non-primitive) in its serialized JSON form
spring.gson.exclude-fields-without-expose-annotation=false # Configures Gson to exclude all fields from consideration for serialization or deserialization that do not have the Expose annotation.
spring.gson.field-naming-policy= # Configures Gson to apply a specific naming policy to an object's field during serialization and deserialization.
spring.gson.generate-non-executable-json=false # Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some special text.
spring.gson.lenient=false # By default, Gson is strict and only accepts JSON as specified by RFC 4627. This option makes the parser liberal in what it accepts.
spring.gson.long-serialization-policy= # Configures Gson to apply a specific serialization policy for Long and long objects.
spring.gson.pretty-printing=false # Configures Gson to output Json that fits in a page for pretty printing. This option only affects Json serialization.
spring.gson.serialize-nulls=false # Configure Gson to serialize null fields.
# JERSEY ({sc-spring-boot-autoconfigure}/jersey/JerseyProperties.{sc-ext}[JerseyProperties])
spring.jersey.application-path= # Path that serves as the base URI for the application. If specified, overrides the value of "@ApplicationPath".
spring.jersey.filter.order=0 # Jersey filter chain order.

Loading…
Cancel
Save