Spring Boot version: 2.5.0

Details

I have custom ObjectMapper defined as:

@Bean
@Primary
public ObjectMapper jacksonMapper() {
    ObjectMapper mapper = new ObjectMapper();
    registerJacksonModules(mapper);
    return mapper;
}

// ...

private void registerJacksonModules(ObjectMapper mapper) {
    // ...
    KotlinModule kotlinModule = new KotlinModule.Builder()
            .singletonSupport(SingletonSupport.CANONICALIZE)
            .build();
    mapper.registerModule(kotlinModule);
}

When I use mapper myself and deserialize, say, a file, things are OK -- object is re-used:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
    JsonSubTypes.Type(SomeConfigStrategy.Yes::class, name = "YES"),
    JsonSubTypes.Type(SomeConfigStrategy.No::class, name = "NO"),
)
sealed class SomeConfigStrategy {
    object Yes : SomeConfigStrategy()
    object No : SomeConfigStrategy()
}

// ...

class SomeConfig @JvmOverloads constructor(
    var param1: SomeConfigStrategy = SomeConfigStrategy.Yes,
)

// ...

val config: SomeConfig = mapper.readValue(configFile) // OK, param1 is the same instance as `Yes`/`No`

But when I try to load this config from URL this does not work and instances of Yes/No are created instead:

// server
@GetMapping("api/path")
fun constructSomeConfig(): SomeConfig {
    return /* construction of config*/
}

// client
fun getConfig(): SomeConfig = restTemplate.getForObject(configUri) // BAD, param1 is NOT the same instance as `Yes`/`No`

Expected behavior

param1 must be deserialized as the same instance of Yes/ No because they are object-s

Observer behavior

param1 is deserialized as new instance every time which is different from "static" SomeConfigStrategy.Yes / SomeConfigStrategy.No

Steps to reproduce

(I will create a minimalistic example later on, if needed)

Comment From: snicoll

@smedelyan thanks but a sample is needed as you're not showing on the RestTemplate was built. I'd recommend not pasting a large amount of code snippet and go ahead with the sample directly.

Comment From: smedelyan

@snicoll uhm, it turned out I was building RestTemplate with default message converters like:

return RestTemplateBuilder()
    .setConnectTimeout(/* ... */)
    .setReadTimeout(/* ... */)
    .build()

This way it uses default list of converters and default mapper -- which, although, is configured with KotlinModule() but singletonSupport seems to have default value DISABLED. I ended up injecting MappingJackson2HttpMessageConverter from context (which has custom ObjectMapper configured) and solved the issue:

@Bean
fun restTemplate(converters: MappingJackson2HttpMessageConverter): RestTemplate {
    return RestTemplateBuilder()
        .setConnectTimeout(/* ... */)
        .setReadTimeout(/* ... */)
        .messageConverters(converters) // <--- here
        .build()
}

I'm closing the issue, thanks for your feedback.

Comment From: snicoll

Thanks for following up. FWIW, that's not the idiomatic way of configuring the RestTemplate. You should inject the RestTemplateBuilder rather than creating it yourself.