ClassCastException with enums in REST server application

I’m facing a strange error in a REST server application when trying to store an entity instance in a database:

The entity contains a map with enums as keys and strings as values. Both the entity and the enum class come from generated code via an OpenAPI YAML file; I only added a few annotations such as @Entity, @Id, @ElementCollection etc. so that I’m able to store and retrieve data via Hibernate. So far, so good, calling my REST server code to store and retrieve instances via the REST interface works.

When I add the Jackson databind annotations @JsonSerialize to the entity map’s getter method and @JsonDeserialize to the corresponding setter method to use a custom (de-)serializer for the map values, storing an entity instance in my server code now fails in its database layer:

java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Enum (java.lang.Integer and java.lang.Enum are in module java.base of loader 'bootstrap')
	at org.hibernate.type.descriptor.java.EnumJavaType.unwrap(EnumJavaType.java:36)
	at org.hibernate.type.descriptor.jdbc.TinyIntJdbcType$1.doBind(TinyIntJdbcType.java:75)
	at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
	at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.lambda$beforeStatement$0(JdbcValueBindingsImpl.java:87)
	at java.base/java.lang.Iterable.forEach(Iterable.java:75)
	at org.hibernate.engine.jdbc.mutation.spi.BindingGroup.forEachBinding(BindingGroup.java:51)
	at org.hibernate.engine.jdbc.mutation.internal.JdbcValueBindingsImpl.beforeStatement(JdbcValueBindingsImpl.java:85)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:130)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:55)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55)
	at org.hibernate.persister.collection.mutation.InsertRowsCoordinatorStandard.insertRows(InsertRowsCoordinatorStandard.java:117)
	at org.hibernate.persister.collection.BasicCollectionPersister.recreate(BasicCollectionPersister.java:119)
	at org.hibernate.action.internal.CollectionRecreateAction.execute(CollectionRecreateAction.java:47)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:632)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:499)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:371)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:41)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1425)
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:487)
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2324)
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1981)
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
	at org.hibernate.internal.TransactionManagement.commit(TransactionManagement.java:65)
	at org.hibernate.internal.TransactionManagement.manageTransaction(TransactionManagement.java:23)
	at org.hibernate.SessionFactory.lambda$inTransaction$0(SessionFactory.java:237)
	at org.hibernate.SessionFactory.inSession(SessionFactory.java:217)
	at org.hibernate.SessionFactory.inTransaction(SessionFactory.java:237)
	at com.example.DatabaseManager.insert(DatabaseManager.java:235)
(...)

Store method in my database layer:

public <T> void insert(T data) throws PersistenceException {
	try {
		this.sessionFactory.inTransaction(session -> session.persist(data));
	} catch (Exception ex) {
		throw new PersistenceException("Error saving object", ex);
	}
}

Enum class:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

public enum MyEnum {
	ONE(1), TWO(2), THREE(3);

	private Integer value;

	MyEnum(Integer value) {
		this.value = value;
	}

	public static MyEnum fromString(String s) {
		for (MyEnum b : MyEnum.values()) {
			if (java.util.Objects.toString(b.value).equals(s)) {
				return b;
			}
		}
		throw new IllegalArgumentException("Unexpected string value '" + s + "'");
	}

	@Override
	@JsonValue
	public String toString() {
		return String.valueOf(value);
	}

	@JsonCreator
	public static MyEnum fromValue(Integer value) {
		for (MyEnum b : MyEnum.values()) {
			if (b.value.equals(value)) {
				return b;
			}
		}
		throw new IllegalArgumentException("Unexpected value '" + value + "'");
	}
}

Entity class:

@Entity
public class MyEntity {
	private @Valid @Id @GeneratedValue Long id;

	private @ElementCollection(fetch = FetchType.EAGER) Map<MyEnum, String> data;

	public Long getId() {
		return id;
	}

	public void setId(Long pId) {
		this.id = pId;
	}

	@JsonProperty("data")
	// @JsonSerialize(using = MapToArraySerializer.class)
	public Map<MyEnum, String> getData() {
		return this.data;
	}

	@JsonProperty("data")
	// @JsonDeserialize(using = ArrayToMapDeserializer.class)
	public void setData(Map<MyEnum, String> pData) {
		this.data = pData;
	}

	public void addData(MyEnum key, String value) {
		if (null == this.data) {
			this.data = new HashMap<>();
		}
		this.data.put(key, value);
	}
}

I’ve verified this via a JUnit test that starts up the REST server code before any tests are executed.

What puzzles me:

Annotations are uncommented:

  • The unit test calls the insert method in the database layer directly => works
  • The unit test triggers a REST API call so that the server code tries to insert the entity => exception

Annotations are commented out:
Inserting works in both cases.

Do you have any idea what I’m doing wrong and/or what is causing this behaviour?

Environment:
Hibernate ORM: 6.5.2.Final
RESTeasy 6.2.9.Final
Java 21
Database: PostgreSQL 16.2

Regards

Thorsten

PS: The above mentioned server application code is a simulator for a custom program we have to call via an OpenAPI REST interface. Although the API description says that the entity’s field is a map, the custom program implementation assumes it to be an array (!) with entries [ key_1, value_1, …, key_n, value_n ] with key_X/value_x being the data from map entry #X

AFAICT from the description, it seems to me that Jackson deserializes something into that Map field which is not a Map<MyEnum, String>, but rather Map<Integer, String>, so your ArrayToMapDeserializer probably does something wrong. Note that generics in Java are erased.

D’oh! You’re right, the deserializer indeed created Integer objects as keys instead of the enum instances. I was so sure that this wasn’t the case because I inspected the map in my debugger and had a look at the entries but overlooked that the string representation didn’t show the class type…

Thanks for helping me with this!