How do I force null objects to be serialized as an empty string in my default JSON responses in Spring Boot?
I would like it to show as:
{
myProperty: "",
}
But what comes out by default is:
{
myProperty: null,
}
I don't want myProperty to be excluded from this list, so I'm not interested in changing the JsonInclude.Include.NON_NULL
Here is what I've tried:
In my main WebMvcConfigurerAdapter class:
#Autowired
private NullAsEmptyStringSerializer nullSerializer;
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomSerialization() {
return jacksonObjectMapperBuilder -> {
DefaultSerializerProvider serializerProvider = new DefaultSerializerProvider.Impl();
serializerProvider.setNullValueSerializer(nullSerializer);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializerProvider(serializerProvider);
jacksonObjectMapperBuilder.configure(objectMapper);
};
And then my nullSerializer object class:
#Component
public class NullAsEmptyStringSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("\"\"");
}
}
The problem is that the serialize() method is never called on my custom class.
The problem with this configuration through Jackson2ObjectMapperBuilderCustomizer lies in these lines:
ObjectMapper objectMapper = new ObjectMapper();
jacksonObjectMapperBuilder.configure(objectMapper);
In fact, this has no effect, because the builder always uses its own instance of ObjectMapper, created internally. If we check in the source code of the builder:
public <T extends ObjectMapper> T build() {
ObjectMapper mapper;
// conditional instantiation of mapper
configure(mapper);
return (T) mapper;
}
Our previous instance of ObjectMapper is just ignored.
Unfortunately, there is no way to set SerializerProviders through Jackson2ObjectMapperBuilder. Probably, this feature is not yet implemented in the API.
But there are at least two possible solutions for the problem.
First, according to the official Spring docs, you can always create your own ObjectMapper bean marked with #Primary and configured fully as you wish, for example:
#Bean
#Primary
public ObjectMapper objectMapper() {
DefaultSerializerProvider serializerProvider = new DefaultSerializerProvider.Impl();
serializerProvider.setNullValueSerializer(nullSerializer);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializerProvider(serializerProvider);
return objectMapper;
}
This will enable the nullSerializer properly, but the drawback is that Spring's auto-configuration of the ObjectMapper instance will be lost and that is probably not what you always want.
The second solution goes through bootstrapping the default instance of ObjectMapper and setting the desired property on it. This can be achieved by implementing the InitializingBean interface, in quite an easy way:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
#Configuration
public class ObjectMapperConfig implements InitializingBean {
#Autowired
private NullAsEmptyStringSerializer nullSerializer;
#Autowired
private ObjectMapper objectMapper;
// will be called by Spring after all the beans are created
// and the proper `objectMapper` instance is available here.
#Override
public void afterPropertiesSet() throws Exception {
DefaultSerializerProvider serializerProvider = new DefaultSerializerProvider.Impl();
serializerProvider.setNullValueSerializer(nullSerializer);
objectMapper.setSerializerProvider(serializerProvider);
}
}
Last but not least, there is a small problem in the NullAsEmptyStringSerializer above.
jsonGenerator.writeString("\"\"");
will output "myProperty":"\"\"". This should be changed to
jsonGenerator.writeString("");
Related
Springboot and Axon: Basically I am unit testing an aggregate that uses three different ObjectMapper instances, each with a different configuration. These are defined in config class :
#Configuration
public class JacksonConfiguration {
#Bean(name="flatMapper")
#Primary
public ObjectMapper flatMapper(){
return new ObjectMapper();
}
#Bean(name="unknownPropertiesMapper")
public ObjectMapper unknownPropertiesMapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
#Bean(name="nullPropertiesMapper")
public ObjectMapper nullPropertiesMapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
}
They are injected and used in my aggregate as follow:
#Aggregate
#Data
#Component
public class MyAggregate {
#Autowired
#Qualifier("flatMapper")
private ObjectMapper flatMapper;
#Autowired
#Qualifier("unknownPropertiesMapper")
private ObjectMapper unknownPropertiesMapper;
#Autowired
#Qualifier("nullPropertiesMapper")
private ObjectMapper nullPropertiesMapper;
#AggregateIdentifier
private String id;
//Methods and Handlers: a method is using "unknownPropertiesMapper" is "changedKeySet"
when I run SpringBootApplication everything is properly instanciated and working as expected, but when testing I get NullPointerException over thier instances:
#ContextConfiguration(classes = JacksonConfiguration.class)
#SpringBootTest(classes = OccurrenceAggregate.class)
#ComponentScan(basePackageClasses = MyAggregate.class)
public class AggregateTest {
private FixtureConfiguration<MyAggregate> fixture;
#BeforeEach
public void setUp() {
fixture = new AggregateTestFixture<>(MyAggregate.class);
}
#Test
public void myTest() {
fixture.givenNoPriorActivity()....
}
test console:
org.axonframework.test.AxonAssertionError: The command handler threw an unexpected exception
Expected <ANYTHING> but got <exception of type [NullPointerException]>. Stack trace follows:
java.lang.NullPointerException
at com.example.business.aggregates.MyAggregate.changedKeySet(MyAggregate.java:185)
changedKeySet() is throwing NPE because its using unknownPropertiesMapper and it is value is null.
as I mentionned it works fine when I run the Main class but not in tests (Junit5).
The Aggregate is not set up correctly. The correct way to inject a Spring Bean into the Aggregate is to add it to the CommandHandler method.
#CommandHandler
public void handle(ACommand cmd, ObjectMapper flatMapper) {
In the test fixture you can inject it this way:
fixture.registerInjectableResource(flatMapper);
I'm trying to inject my config into my custom StdDeserializer. The problem however is, even though I marked the class as #Component, the value of the field is never injected. The injection works without any problems in the other places of the application.
Therefore, I've come to the conclusion that the problem is with the way the deserializer is working, since it doesn't get instantiated by us but rather like the example down here:
ClassToDeserialize myClass = new ObjectMapper().readValue(mockJson, ClassToDeserialize.class);
As you can see there is no explicit usage of my custom deserializer ClassToDeserializeDeserializer hence it detects the classes with the custom deserializer with the #JsonDeserialize(using = ClassToDeserialize.class) annotation.
Class that should be deserialized
#Data
#AllArgsConstructor
#SuperBuilder
#JsonDeserialize(using = MyClassDeserializer.class)
public class MyClass{
private final String field1;
private final String field2;
}
Config class that should be injected
#Configuration
#ConfigurationProperties(prefix = "myconfig")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class MyConfig {
private final String confField1;
private final String confField2;
}
MyClass's custom deserializer:
#Component
public class MyClassDeserializer extends StdDeserializer<MyClass> {
#Autowired
MyConfig myConfig;
public MyClassDeserializer () {
this(null);
}
public MyClassDeserializer (Class<?> vc) {
super(vc);
}
#Override
public MyClassDeserializer deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
//Deserializing code that basically tries to read from myConfig
myConfig.getConfField1(); //Hello NullPointerException my old friend
}
}
Usage of the deserializer
MyClass myClass = new ObjectMapper().readValue(mockJson, MyClass.class);
Reason why it doesn't work:
Jackson doesn't know anything about Spring Boot stuff, so when you readValue(..) Jackson sees #JsonDeserialize annotation with deserializer class and creates new instance of the deserializer (it doesn't pick up the bean, but rather just new MyClassDeserializer(..)), that is why you never see MyConfig being injected.
If you want to make it work, you need to somehow register this deserializer through Spring Boot, for example, like this: How to provide a custom deserializer with Jackson and Spring Boot
I am trying to implement generic Spring trim serializer across application however its doesn't seems to be working.
And if I manually put this serializer #JsonSerialize(using = StringTrimmerSerializer.class) on a particular field it does work not sure what i need to do to make it work throughout application without putting it for all fields individually
import java.io.IOException;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.jackson.JsonComponent;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
#JsonComponent
public class StringTrimmerSerializer extends JsonSerializer<String> {
#Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (!StringUtils.isEmpty(value)) {
value = value.trim();
}
gen.writeString(value);
}
}
Update:
Tried registering serializer as well but same issue
#Configuration
public class JacksonConfiguration {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper(); //
//mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.registerModule(new SimpleModule().addSerializer(String.class, new StringTrimmerSerializer()));
return mapper;
}
/*
* #Bean public Module customSerializer() { SimpleModule module = new
* SimpleModule(); module.addSerializer(String.class, new
* StringTrimmerSerializer()); return module; }
*/
}
Main Class package : com.demo
Serializer Package : com.demo.config
Spring boot Version - 2.2.5.RELEASE
Jackson-databind - 2.10.2
Add constructors in StringTrimmerSerializer
public StringTrimmerSerializer ()
public StringTrimmerSerializer (Class<String> s) {
super(s);
}
I was able to resolve by registering custom serializer to jaskcosn's default object mapper rather than creating a new reference of ObjectMapper.
#Configuration
public class JacksonConfiguration extends WebMvcConfigurationSupport {
#Autowired
private ObjectMapper objectMapper;
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
objectMapper.registerModule(new SimpleModule().addSerializer(String.class, new StringTrimmerSerializer()));
converter.setObjectMapper(objectMapper);
return converter;
}
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
}
}
I have a Spring Boot web app with several #RestController classes.
I like the default json format returned by my REST controllers.
For use in my DAO beans (which do json serialization and deserialization ), I have created a custom ObjectMapper:
#Configuration
public class Config{
#Bean
public ObjectMapper getCustomObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
return objectMapper;
}
}
And in each of my DAO classes I autowire my custom ObjectMapper:
#Repository
#Transactional
public class MyDaoImpl implements MyDao {
#Autowired
ObjectMapper objectMapper
//Dao implementation...
}
This all works fine. The problem is that my custom ObjectMapper gets automatically picked up by Spring and is used for serializing REST responses.
This is undesirable. For REST controllers I want to keep the ObjectMapper that Spring creates by default.
How can I tell Spring Boot to not detect and not use my custom ObjectMapper bean for its own internal workings?
The Simone Pontiggia answer is in the correct direction. You should create one #Primary bean, which Spring will use in its internals, and then to create your own ObjectMapper beans and autowired them using #Qualifier.
The problem here is that, creating default bean like:
#Bean
#Primary
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
Won't actually work as expected, because the Spring default ObjectMapper has additional configurations.
The correct way to create default ObjectMapper that will be used by spring, is:
#Bean
#Primary
public ObjectMapper objectMapper() {
return Jackson2ObjectMapperBuilder.json().build();
}
You can find more information about the Spring default ObjectMapper here: https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html under 79.3 Customize the Jackson ObjectMapper
Since I didn't want to touch Spring's default ObjectMapper, creating a #Primary ObjectMapper to shadow Spring's default ObjectMapper was out of the question.
Instead, what I ended up doing is creating a BeanFactoryPostProcessor which registers in Spring's context a custom, non primary ObjectMapper:
#Component
public class ObjectMapperPostProcessor implements BeanFactoryPostProcessor {
public static final String OBJECT_MAPPER_BEAN_NAME = "persistenceObjectMapper";
#Override
public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) {
final AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(ObjectMapper.class, this::getCustomObjectMapper)
.getBeanDefinition();
// Leave Spring's default ObjectMapper (configured by JacksonAutoConfiguration)
// as primary
beanDefinition.setPrimary(false);
final AutowireCandidateQualifier mapperQualifier = new AutowireCandidateQualifier(PersistenceObjectMapper.class);
beanDefinition.addQualifier(mapperQualifier);
((DefaultListableBeanFactory) beanFactory).registerBeanDefinition(OBJECT_MAPPER_BEAN_NAME, beanDefinition);
}
private ObjectMapper getCustomObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
return objectMapper;
}
}
As can be seen in the code above, I also assigned a qualifier to my custom ObjectMapper bean.
My qualifier is an annotation which is annotated with #Qualifier:
#Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Qualifier
public #interface PersistenceObjectMapper {
}
I can then autowire my custom ObjectMapper using my custom annotation, like this:
#Repository
public class MyDao {
#Autowired
public MyDao(DataSource dataSource, #PersistenceObjectMapper ObjectMapper objectMapper) {
// constructor code
}
You can provide a standard ObjectMapper and your customized object mapper, and set the standard as #Primary.
Then gives your custom ObjectMapper a name and use it with #Qualifier annotation.
#Configuration
public class Config{
//This bean will be selected for rest
#Bean
#Primary
public ObjectMapper stdMapper(){
return new ObjectMapper();
}
//You can explicitly refer this bean later
#Bean("customObjectMapper")
public ObjectMapper getCustomObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.SnakeCaseStrategy());
return objectMapper;
}
}
Now you can reference your custom mapper
#Repository
#Transactional
public class MyDaoImpl implements MyDao {
#Autowired
#Qualifier("customObjectMapper")
ObjectMapper objectMapper
//Dao implementation...
}
#Resource("custonmObjectMapper") will do the same of #Autowired and #Qualifier together
You can create:
public class MapperUtils {
private static final ObjectMapper mapper = new ObjectMapper();
public static <T> T parseResponse(byte[] byteArrray, Class<T> parseType) throws JsonParseException, JsonMappingException, IOException {
return mapper.readValue(byteArrray, parseType);
}
}
ObjectMapper is thread-safe. However, some people discourage having single instance because of performance issues (Should I declare Jackson's ObjectMapper as a static field? ).
When I'm trying to deserialize Object from a String and this String does not contain certain fields or has fields that are not in my Object, Jackson serializer is completely okay with that and just creates my Object with null/Optional.empty() fields, also ignoring unkown properties. I tried to set reader with feature FAIL_ON_UNKNOWN_PROPERTIES but to no success. I have fairly simple Jackson configuration, not much besides adding support for Java 8 and java.time.
Edit:
public final ObjectReader reader;
public final ObjectWriter writer;
private JsonMapperTestInstance() {
ObjectMapper mapper = new JacksonConfiguration().objectMapper();
reader = mapper.reader();
writer = mapper.writer().withFeatures(SerializationFeature.INDENT_OUTPUT);
}
public <T> T deserialize(Class<T> actual, String serialized) throws IOException {
return reader.forType(actual).readValue(serialized);
}
JacksonConfiguration:
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
registerModules(mapper);
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
return mapper;
}
#Bean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
#Bean
public Jdk8Module jdk8Module() {
return new Jdk8Module().configureAbsentsAsNulls(true);
}
private void registerModules(ObjectMapper mapper) {
mapper.registerModule(jdk8Module());
mapper.registerModule(javaTimeModule());
}
#Primary
#Bean
public ObjectWriter writer(ObjectMapper mapper) {
return mapper.writer();
}
#Primary
#Bean
public ObjectReader reader(ObjectMapper mapper) {
return mapper.reader();
}
I have determined that annotation #JasonUnwrapped is causing this behaviour. Without it Jackson throws expection on property "very_wrong_field", which previously was silently ignoring.