How to customize the Hypersistence Utils Jackson ObjectMapper

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

The Hypersistence Utils open-source project allows you to map JSON, and ARRAY when using JPA and Hibernate.

Since I launched this project, one of the most wanted features was to add support for customizing the underlying Jackson ObjectMapper, and since version 2.1.1, this can be done either declaratively or programmatically.

In this article, you are going to see how to customize the ObjectMapper when using the Hypersistence Utils project.

Declarative configuration

The easiest way to achieve this goal is to set the hypersistence.utils.jackson.object.mapper configuration property in the hibernate.properties file:

hypersistence.utils.jackson.object.mapper=com.vladmihalcea.hibernate.type.json.loader.CustomObjectMapperSupplier

This property takes the fully-qualified class name of an implementation of the ObjectMapperSupplier interface:

What’s nice about customizing the Hypersistence Utils project is that you can use either the Hibernate-specific hibernate.properties or you can provide a hypersistence-utils.properties file if you cannot alter hibernate.properties.

You can even supply a different Java properties file via the hypersistence-utils.properties.path System property.

The CustomObjectMapperSupplier can look as follows:

public class CustomObjectMapperSupplier 
    implements ObjectMapperSupplier {

    @Override
    public ObjectMapper get() {
        ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
        
        objectMapper.setTimeZone(
            TimeZone.getTimeZone("GMT")
        );
        SimpleModule simpleModule = new SimpleModule(
            "SimpleModule", 
            new Version(1, 0, 0, null, null, null)
        );
        simpleModule.addSerializer(new MoneySerializer());
        objectMapper.registerModule(simpleModule);
        
        return objectMapper;
    }
}

Here, we are using a custom ObjectMapper that uses our own MoneySerializer for handling BigDecimal Java Object types:

public class MoneySerializer 
    extends JsonSerializer<BigDecimal> {

    @Override
    public void serialize(
                BigDecimal value, 
                JsonGenerator jsonGenerator, 
                SerializerProvider provider)
            throws IOException {
        jsonGenerator.writeString(
            value.setScale(2, BigDecimal.ROUND_HALF_UP).toString()
        );
    }

    @Override
    public Class<BigDecimal> handledType() {
        return BigDecimal.class;
    }
}

So, if you are persisting the following Book entity which has a JSON properties attribute:

Book book = new Book();
book.setIsbn("978-9730228236");
book.setProperties(
    JacksonUtil.toJsonNode("""
        {
           "title": "High-Performance Java Persistence",
           "author": "Vlad Mihalcea",
           "publisher": "Amazon",
           "price": 44.991234567 
        }
	"""
    )
);

entityManager.persist(book);

When fetching the Book entity, we can see that the BigDecimal takes only two decimals:

Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Book.class)
.load("978-9730228236");

assertEquals(
    "44.99", 
    book.getProperties().get("price").asText()
);

Programmatic configuration

For even more flexibility, you can use the programmatic configuration.

Assuming we have the following Object type that we want to serialize as JSON:

public class Location 
    implements Serializable {

    private String country;

    private String city;

    private BigDecimal reference;

    //Getters and setters omitted for brevity
}

And we have an Event entity that maps the Location Java Object to a jsonb PostgreSQL column type:

@Entity(name = "Event")
@Table(name = "event")
public class Event {

    @Id
    private Long id;

    @Type(type = "location")
    @Column(columnDefinition = "jsonb")
    private Location location;

    //Getters and setters omitted for brevity
}

We can use a custom ObjectMapper as follows:

JsonType jsonType = new JsonType(
    new CustomObjectMapperSupplier().get(), 
    Location.class
);

properties.put("hibernate.type_contributors",
    (TypeContributorList) () -> Collections.singletonList(
        (typeContributions, serviceRegistry) ->
            typeContributions.contributeType(
                jsonType, "location"
            )
    )
);

The hibernate.type_contributors configuration property allows you to register custom Hibernate Types, and we can use this feature to provide our own custom ObjectMapper to the JsonType provided by the Hypersistence Utils project.

So, if we persist the following Event entity:

Location location = new Location();
location.setCountry("Romania");
location.setCity("Cluj-Napoca");
location.setReference(
    BigDecimal.valueOf(2.25262562526626D)
);

Event event = new Event();
event.setId(1L);
event.setLocation(location);

entityManager.persist(event);

When fetching the Event entity, we can see that the BigDecimal takes only two decimals:

Event event = entityManager.find(Event.class, 1L);

assertEquals("2.25", event.getLocation().getReference().toString());

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Not only that you can customize the ObjectMapper when using the Hypersistence Utils project, but the new configuration mechanism will allow for customizing other behaviors as well.

So, stay tuned for more!

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.