Querydls Case When enum: Could not determine ValueMapping for SqmParameter

I have a query made with querydsl, in the query I extract a field as an enum, but running the query it throws this error:

org.hibernate.query.sqm.sql.ConversionException: Could not determine ValueMapping for SqmParameter: SqmPositionalParameter(2)

The problem seems related to the conversion of the enum. The query is something like this:

query.select(new QTestDto(qTE.id,
           Expressions.cases()
                      .when(qTE.code.startsWith("A"))
                      .then(TestType.GOOD)
                      .otherwise(TestType.BAD)))
     .from(qTE)
     .fetch();

If I change the projected objects TestDto, using a string instead an enum, and I change the extraction with toString(), it works, but I would like to use directly the enum.

This is a unit test to reproduce the error:

import com.querydsl.core.annotations.QueryProjection;
import com.querydsl.core.types.ConstructorExpression;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.PathMetadata;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.jpa.impl.JPAQuery;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.io.Serial;

import static com.querydsl.core.types.PathMetadataFactory.forVariable;
import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class CaseBuilderTest {

  @Autowired
  private EntityManager em;

  @Test
  void test() {

    // given
    em.persist(new TestEntity(1L, "A003", "Description ..."));
    em.persist(new TestEntity(2L, "B538", "Description ..."));

    // when
    var qTE = QTestEntity.testEntity;
    var query = new JPAQuery<TestDto>(em);
    var result = query.select(new QTestDto(qTE.id,
                            Expressions.cases()
                                       .when(qTE.code.startsWith("A"))
                                       .then(TestType.GOOD)
                                       .otherwise(TestType.BAD)))
                      .from(qTE)
                      .fetch();

    // then
    assertThat(result).hasSize(2);
  }

  @Getter
  @Setter
  public static class TestDto {
    private Long id;
    private TestType type;

    @QueryProjection
    public TestDto(Long id, TestType type) {
      this.id = id;
      this.type = type;
    }
  }


  @Entity
  @AllArgsConstructor
  @NoArgsConstructor
  public static class TestEntity {
    @Id
    @Column
    private Long id;
    @Column
    private String code;
    @Column
    private String description;
  }


  public enum TestType {
    GOOD,
    BAD;
  }


  // Generated
  public static class QTestEntity extends EntityPathBase<TestEntity> {
    @Serial
    private static final long serialVersionUID = 1L;
    public static final QTestEntity testEntity = new QTestEntity("testEntity");
    public final StringPath code = createString("code");
    public final StringPath description = createString("description");
    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QTestEntity(String variable) {
      super(TestEntity.class, forVariable(variable));
    }

    public QTestEntity(Path<? extends TestEntity> path) {
      super(path.getType(), path.getMetadata());
    }

    public QTestEntity(PathMetadata metadata) {
      super(TestEntity.class, metadata);
    }
  }

  // Generated
  public static class QTestDto extends ConstructorExpression<TestDto> {

    @Serial
    private static final long serialVersionUID = 1L;

    public QTestDto(com.querydsl.core.types.Expression<Long> id, com.querydsl.core.types.Expression<TestType> type) {
      super(TestDto.class, new Class<?>[] {long.class, TestType.class}, id, type);
    }

  }


}

What am I doing wrong?

The error you are encountering, ConversionException: Could not determine ValueMapping for SqmParameter, typically arises when Hibernate cannot properly convert or map an enum type to a database-compatible format in your QueryDSL query. Here’s how to address the issue and ensure proper handling of enums in your case.

Understanding the Issue

  1. Enum Handling: When using enums in QueryDSL, Hibernate needs to know how to map these enums to a database-compatible type. By default, Hibernate may not have a clear mapping for the enum values, especially if you’re directly using them in expressions.
  2. SQL Type Mapping: Enums in JPA can be mapped to different database types (e.g., String or Ordinal). If Hibernate doesn’t know how to handle the enum type in your query context, you may encounter the conversion exception.

Solution: Explicitly Define the Enum Mapping

To resolve this issue, ensure that you explicitly define how the enum should be mapped to the database. Here are a few strategies you can use:

1. Use JPA Annotations to Specify Enum Mapping

Make sure your TestEntity class has the enum field explicitly annotated. You can do this using @Enumerated to define how the enum should be stored in the database.

import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;

@Entity
@AllArgsConstructor
@NoArgsConstructor
public static class TestEntity {
    @Id
    @Column
    private Long id;

    @Column
    private String code;

    @Column
    private String description;

    @Enumerated(EnumType.STRING) // or EnumType.ORDINAL, depending on your preference
    private TestType type; // Add this field if you want to store the enum in the entity
}

2. Modify the Query to Return Enum Values as Strings

If you want to keep the mapping from the query without directly modifying the TestEntity, you can modify the QueryDSL query to convert enum values to strings:

var result = query.select(new QTestDto(qTE.id,
                            Expressions.cases()
                                       .when(qTE.code.startsWith("A"))
                                       .then(TestType.GOOD.toString()) // Convert to String
                                       .otherwise(TestType.BAD.toString()))) // Convert to String
                      .from(qTE)
                      .fetch();

3. Verify QueryDSL Configuration

Ensure your QueryDSL setup is properly configured to handle enums. Sometimes, the QueryDSL configurations in your pom.xml or build settings might affect how enums are handled in queries. Ensure you are using compatible versions of QueryDSL and Hibernate.

Conclusion

By ensuring that enums are properly mapped in your JPA entities and possibly adjusting how enums are handled in your QueryDSL queries, you should be able to resolve the ConversionException. The critical aspect is ensuring that Hibernate has a clear path for mapping enums to a database-compatible format.