In almost every Spring Boot project I’ve worked on, JSON is used extensively — whether it’s for logging, testing or exchanging data. To avoid injecting ObjectMapper repeatedly, I use a lightweight static helper: Json.stringify() and Json.parse().

1. Configure the Jackson ObjectMapper

This configures a production-safe ObjectMapper with readable dates, no nulls, and safe deserialization. Jackson2ObjectMapperBuilder includes JavaTimeModule by default, so date/time types like LocalDate and Instant work out of the box. You can still register additional modules as needed.

@Configuration
public class JacksonConfig {

    @Bean
    @Primary
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        return builder
                .modulesToInstall(AfterburnerModule.class)
                .serializationInclusion(JsonInclude.Include.NON_NULL)
                .featuresToDisable(
                        SerializationFeature.INDENT_OUTPUT,
                        SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                        DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
                )
                .build();
    }
}

2. Build the Utility

This Spring bean is the actual utility that uses the configured ObjectMapper. It exposes simple methods for serialization and deserialization, returning Optional<T> to avoid unchecked exceptions.

@Component
@Slf4j
@RequiredArgsConstructor
public class JacksonUtils {

    private final ObjectMapper mapper;

    public String toJson(Object obj) {
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException("Failed to serialize object", e);
        }
    }

    public <T> Optional<T> fromJson(String json, Class<T> clazz) {
        try {
            return Optional.ofNullable(mapper.readValue(json, clazz));
        } catch (JsonProcessingException e) {
            log.warn("Failed to parse JSON to {}: {}", clazz.getSimpleName(), json, e);
            return Optional.empty();
        }
    }

    public <T> Optional<T> fromJson(String json, TypeReference<T> typeRef) {
        try {
            return Optional.ofNullable(mapper.readValue(json, typeRef));
        } catch (JsonProcessingException e) {
            log.warn("Failed to parse JSON with typeRef {}: {}", typeRef, json, e);
            return Optional.empty();
        }
    }
}

3. Expose Static Access

This is the static utility class you’ll actually use (see @UtilityClass from Lombok). It delegates to JacksonUtils and acts as a facade over ObjectMapper, making JSON handling clean and concise in static contexts, tests, and scripts.

@UtilityClass
public class Json {

    @Setter
    private JacksonUtils delegate;

    private JacksonUtils getDelegate() {
        if (delegate == null) {
            throw new IllegalStateException("JacksonUtils delegate is not initialized yet.");
        }
        return delegate;
    }

    public String stringify(Object obj) {
        return getDelegate().toJson(obj);
    }

    public <T> Optional<T> parse(String json, Class<T> clazz) {
        return getDelegate().fromJson(json, clazz);
    }

    public <T> Optional<T> parse(String json, TypeReference<T> typeRef) {
        return getDelegate().fromJson(json, typeRef);
    }

    public <T> TypeReference<T> typeRef() {
        return new TypeReference<>() {};
    }
}

4. Hook into Spring

This class bridges the Spring-managed JacksonUtils with the static Json class by setting it as a delegate once the context is ready. This guarantees safe usage in any part of the app.

@Component
@RequiredArgsConstructor
public class JacksonInitializer {

    private final JacksonUtils jacksonUtils;

    @PostConstruct
    public void init() {
        Json.setDelegate(jacksonUtils);
    }
}

5. Use It

Here’s how you might use this utility in an integration test:

@SpringBootTest
class JsonIntegrationTest {

    @Test
    void shouldSerializeAndDeserialize() {
        MyDto dto = new MyDto("value");
        String json = Json.stringify(dto);

        Optional<MyDto> result = Json.parse(json, MyDto.class);

        assertTrue(result.isPresent());
        assertEquals("value", result.get().getField());
    }

    @Test
    void shouldHandleGenericTypes() {
        List<String> input = List.of("one", "two");
        String json = Json.stringify(input);

        Optional<List<String>> result = Json.parse(json, Json.typeRef());

        assertTrue(result.isPresent());
        assertEquals("one", result.get().getFirst());
    }
}