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());
}
}