ObjectMapperHolder.java

package access.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.cfg.ConstructorDetector;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.datatype.hibernate7.Hibernate7Module;

@Configuration
public class ObjectMapperHolder {

    // Jackson 3 defaults java.time types to ISO-8601 strings (DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS
    // defaults to false, vs. true for a hand-built Jackson 2 ObjectMapper); enabling it here preserves
    // the numeric-timestamp wire format this app has always produced.
    public static final JsonMapper objectMapper = JsonMapper.builder()
            .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            // Jackson 3 defaults FAIL_ON_NULL_FOR_PRIMITIVES to true (Jackson 2 defaulted to false);
            // this app's API accepts JSON null for primitive boolean/int fields, so keep the lenient behavior.
            .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false)
            .configure(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, true)
            // Jackson 3's ConstructorDetector.DEFAULT now allows treating a non-default constructor as an
            // implicit properties-based creator even when a no-arg constructor also exists (Jackson 2 did
            // not). Several entities (e.g. JoinRequest) have a Lombok no-arg constructor plus a business
            // constructor whose parameter names happen to match JSON property names; without this, Jackson
            // picks the business constructor for deserialization instead of the no-arg + setters.
            .constructorDetector(ConstructorDetector.DEFAULT.withAllowImplicitWithDefaultConstructor(false))
            .addModule(new Hibernate7Module())
            .build();

    // Declared as JsonMapper (not ObjectMapper) so Boot's JacksonAutoConfiguration#jacksonJsonMapper,
    // which is @ConditionalOnMissingBean against the JsonMapper type, backs off instead of registering
    // its own competing @Primary bean.
    @Bean
    @Primary
    public JsonMapper objectMapper() {
        return objectMapper;
    }

}