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

Jackson2ObjectMapperBuilder's modulesToInstall function does not eventually override the default configuration #22576

Closed
elue opened this issue Mar 11, 2019 · 7 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Milestone

Comments

@elue
Copy link

elue commented Mar 11, 2019

Spring Boot version: 2.1.2.RELEASE

The Jackson2ObjectMapperBuilder's modulesToInstall documentation says

 *Specify one or more modules to be registered with the {@link ObjectMapper}.
 * <p>Modules specified here will be registered after
 * Spring's autodetection of JSR-310 and Joda-Time, or Jackson's
 * finding of modules (see {@link #findModulesViaServiceLoader}),
 * allowing to eventually override their configuration.

But when we have this config:

    @Bean
    public ObjectMapper restApiObjectMapper(final Jackson2ObjectMapperBuilder builder,
                                            final LenientOffsetDateTimeDeserializer offsetDateTimeDeserializer) {
        final JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(OffsetDateTime.class, offsetDateTimeDeserializer);

        builder.modulesToInstall(javaTimeModule);
        builder.failOnUnknownProperties(false);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return builder.build();
    }

It is expect that the custom DateTimeDeserializer will get picked up. But still the default one's are used and leading to json parse errors.

@wilkinsona
Copy link
Member

Jackson2ObjectMapperBuilder is part of Spring Framework and just auto-configured by Spring Boot. From what you've described thus far, it's not clear if the problem is caused by how Spring Boot is configuring Jackson2ObjectMapperBuilder, a bug in Spring Framework, or a problem in your own code. To help us to home in on the problem, can you please provide a complete and minimal sample that reproduces it as a zip attached to this issue or in a separate repository that we can clone.

@elue
Copy link
Author

elue commented Mar 11, 2019

@wilkinsona , thanks for the response. I will put it as a separate repository as soon as possible.

@elue
Copy link
Author

elue commented Mar 11, 2019

I have created a demo app with 2 object mapper configs. https://github.com/elue/object-mapper-demo.git

@wilkinsona
Copy link
Member

Thanks for the sample. It can be made quite a bit more minimal, removing Spring Boot's involvement entirely:

package com.github.example;

import static junit.framework.TestCase.assertNotNull;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

public class Jackson2ObjectMapperBuilderModuleOverrideTests {

    private static final String DATA = "{\"offsetDateTime\": \"2020-01-01T00:00:00\"}";

    @Test
    public void testObjectMapperForZoneOffset() throws IOException {
    	Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer());
        builder.modules(javaTimeModule);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        ObjectMapper objectMapper =  builder.build();
        DemoPojo demoPojo = objectMapper.readValue(DATA, DemoPojo.class);
        assertNotNull(demoPojo.getOffsetDateTime());
    }

    @Test
    public void testFailingObjectMapperForZoneOffset() throws IOException {
    	Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer());
        builder.modulesToInstall(javaTimeModule);
        builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        ObjectMapper objectMapper =  builder.build();
        DemoPojo demoPojo = objectMapper.readValue(DATA, DemoPojo.class);
        assertNotNull(demoPojo.getOffsetDateTime());
    }

}

class OffsetDateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {

    private static final String CURRENT_ZONE_OFFSET = OffsetDateTime.now().getOffset().toString();

    @Override
    public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        final String value = jsonParser.getValueAsString();
        if (StringUtils.isEmpty(value)) {
            return null;
        }
        try {
            return OffsetDateTime.parse(value);

        } catch (DateTimeParseException exception) {
            return OffsetDateTime.parse(value + CURRENT_ZONE_OFFSET);
        }
    }
}

@JsonDeserialize
class DemoPojo {

    private OffsetDateTime offsetDateTime;

	public OffsetDateTime getOffsetDateTime() {
		return offsetDateTime;
	}

	public void setOffsetDateTime(OffsetDateTime offsetDateTime) {
		this.offsetDateTime = offsetDateTime;
	}

}

testFailingObjectMapperForZoneOffset will fail because the registration of the default JavaTimeModule prevents the custom module with the same typeId from being registered. This looks like a bug in Jackson2ObjectMapperBuilder to me. Well-known modules are registered first which, in Jackson 2.9 at least, prevents the custom module from being registered.

@elue
Copy link
Author

elue commented Mar 12, 2019

Thanks for the feedback. Would you raise an issue for the spring framework then?

@wilkinsona
Copy link
Member

I've asked @philwebb to move this issue over to spring-projects/spring-framework.

@snicoll snicoll transferred this issue from spring-projects/spring-boot Mar 12, 2019
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 12, 2019
@sbrannen sbrannen added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Mar 12, 2019
@jhoeller jhoeller added this to the 5.1.6 milestone Mar 12, 2019
@jhoeller
Copy link
Contributor

This sounds worth backporting to 5.0.13 and 4.3.23 as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: backported An issue that has been backported to maintenance branches type: bug A general bug
Projects
None yet
Development

No branches or pull requests

6 participants