How to map JSON collections using JPA and Hibernate

(Last Updated On: January 8, 2018)

Introduction

The open-source hibernate-types project allows you to map Java objects or Jackson JsonNode as JPA or Hibernate entity properties, and, thanks to our awesome contributors, we have added support for storing type-safe JSON collections.

In this article, you are going to see how to achieve this goal.

Maven dependency

First of all, you need to set up the following Maven dependency in your project pom.xml configuration file:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

If you’re using older versions of Hibernate, check out the hibernate-types GitHub repository for more info about the matching dependency for your current Hibernate version.

Domain Model

Let’s assume we have the following Location Java object type.

public class Location implements Serializable {

    private String country;

    private String city;

    //Getters and setters omitted for brevity

    @Override
    public String toString() {
        return "Location{" +
                "country='" + country + ''' +
                ", city='" + city + ''' +
                '}';
    }
}

And, one Event entity:

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

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

    @Type(
        type = "jsonb",
        parameters = {
            @org.hibernate.annotations.Parameter(
                name = TypeReferenceFactory.FACTORY_CLASS,
                value = "com.vladmihalcea.hibernate.type.json.PostgreSQLGenericJsonBinaryTypeTest$AlternativeLocationsTypeReference"
            )
        }
    )
    @Column(columnDefinition = "jsonb")
    private List<Location> alternativeLocations = new ArrayList<Location>();

    //Getters and setters omitted for brevity
}

The BaseEntity defines some basic properties (e.g. @Id, @Version) and several custom Hibernate types, among which, we are interested in the JsonBinaryType one.

@TypeDefs({
    @TypeDef(name = "string-array", typeClass = StringArrayType.class),
    @TypeDef(name = "int-array", typeClass = IntArrayType.class),
    @TypeDef(name = "json", typeClass = JsonStringType.class),
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class),
    @TypeDef(name = "jsonb-node", typeClass = JsonNodeBinaryType.class),
    @TypeDef(name = "json-node", typeClass = JsonNodeStringType.class),
})
@MappedSuperclass
public class BaseEntity {

    @Id
    private Long id;

    @Version
    private Integer version;

    //Getters and setters omitted for brevity
}

For more details about using @MappedSuperclass, check out this article.

TypeReferenceFactory

To store the Location object in a jsonb PostgreSQL column, we just need to annotate the location property with @Type(type = "jsonb").

However, for the alternativeLocations collection, we need to provide the associated Jackson TypeReference so that we can reconstruct the very same type-safe Java collection when reading the JSON object from the relational database.

For this purpose, we provide the fully-qualified class of the TypeReferenceFactory implementation which looks as follows:

public class AlternativeLocationsTypeReference 
    implements TypeReferenceFactory {

    @Override
    public TypeReference<?> newTypeReference() {
        return new TypeReference<List<Location>>() {};
    }
}

That’s it!

Testing time

When saving the following Event entity:

Location cluj = new Location();
cluj.setCountry("Romania");
cluj.setCity("Cluj-Napoca");

Location newYork = new Location();
newYork.setCountry("US");
newYork.setCity("New-York");

Location london = new Location();
london.setCountry("UK");
london.setCity("London");

Event event = new Event();
event.setId(1L);
event.setLocation(cluj);
event.setAlternativeLocations(
    Arrays.asList(newYork, london)
);

entityManager.persist(event);

Hibernate will generate the following SQL INSERT statement:

INSERT INTO event (
    version, 
    alternativeLocations, 
    location, 
    id
) 
VALUES (
    0, 
    [
        {"country":"US","city":"New-York"},
        {"country":"UK","city":"London"}
    ], 
    {"country":"Romania","city":"Cluj-Napoca"}, 
    1
)

Also, when retrieving back the Event entity, both the location and thealternativeLocations` properties are properly fetched:

Event event = entityManager.find(Event.class, eventId);

assertEquals(
    "Cluj-Napoca", 
    event.getLocation().getCity()
);

assertEquals(2, event.getAlternativeLocations().size());

assertEquals(
    "New-York", 
    event.getAlternativeLocations().get(0).getCity()
);
assertEquals(
    "London", 
    event.getAlternativeLocations().get(1).getCity()
);

Cool, right?

If you enjoyed this article, I bet you are going to love my book as well.

Conclusion

The hibernate-types project supports more than JSON types. You can map PostgreSQL ARRAY types or PostgreSQL-specific Enums, nullable Character, or even provide your own immutable Hibernate custom Types.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

10 thoughts on “How to map JSON collections using JPA and Hibernate

    1. It was static because it was nested in the JUnit test class. As for being final, it depends on your use case. Most often, I guess it could be also final as it’s unlikely that you will have to extend it.

  1. Vlad, can you improve `org.hibernate.usertype.DynamicParameterizedType.ParameterType` to add a new method like `java.lang.reflect.Field.getGenericType()` ?
    We can use `objectMapper.getTypeFactory().constructType(type)` to deserialize collections from json.

      1. Why would you want that? I still don’t understand your use case. You can save both Java objects and collections as JSON using this framework. Is there a use case you have that does not work with the current JSON type implementation?

      2. If there is genericReturnClass, jackson can use it infer generic types, and no need TypeReferenceFactory, currently your solution need declare List twice, one for entity and the other for TypeReferenceFactory, It’s superfluous and error prone.

      3. It’s unlikely that anyone will change Hibernate core just to add genericReturnClass so that you avoid this duplication in this example which doe snot even belong to thee official Hibernate framework. You say it’s error prone, but I think that, if you have integration tests the entire data access layer, how could that fail in any way?

      4. It’s just a viable suggestion to make hibernate-types-52 better, and genericReturnClass is necessary in modern java world.

      5. As I already mentioned, if you think it can be improved without requiring changing Hibernate ORM, then send me Pull Request with your proposed changes.

Leave a Reply

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