From ffb7ec574cbbd526429ef4e02801903e8497c503 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Thu, 6 Nov 2025 12:26:57 +0100 Subject: [PATCH 1/2] HHH-19906 Add test for issue --- ...riptiveJsonGeneratingVisitorSmokeTest.java | 99 ++++++++++++++++++- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java index f7411909a40a..c69a8326d252 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/util/DescriptiveJsonGeneratingVisitorSmokeTest.java @@ -13,11 +13,15 @@ import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.spi.DescriptiveJsonGeneratingVisitor; import org.hibernate.type.format.StringJsonDocumentWriter; import org.junit.jupiter.api.AfterAll; @@ -26,6 +30,8 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; @@ -34,12 +40,14 @@ import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -@DomainModel( annotatedClasses = { +@DomainModel(annotatedClasses = { DescriptiveJsonGeneratingVisitorSmokeTest.Company.class, DescriptiveJsonGeneratingVisitorSmokeTest.Address.class, DescriptiveJsonGeneratingVisitorSmokeTest.Employee.class, -} ) + DescriptiveJsonGeneratingVisitorSmokeTest.EntityOfArrays.class, +}) @SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class) public class DescriptiveJsonGeneratingVisitorSmokeTest { private final DescriptiveJsonGeneratingVisitor visitor = new DescriptiveJsonGeneratingVisitor(); @@ -52,7 +60,7 @@ public void testCompany(SessionFactoryScope scope) { .findEntityDescriptor( Company.class ); scope.inTransaction( session -> { - final Company company = session.createQuery( + final Company company = session.createSelectionQuery( "from Company where id = 1", Company.class ).getSingleResult(); @@ -84,7 +92,7 @@ public void testCompanyFetchEmployees(SessionFactoryScope scope) { .findEntityDescriptor( Company.class ); scope.inTransaction( session -> { - final Company company = session.createQuery( + final Company company = session.createSelectionQuery( "from Company c join fetch c.employees where c.id = 1", Company.class ).getSingleResult(); @@ -115,6 +123,64 @@ public void testCompanyFetchEmployees(SessionFactoryScope scope) { } ); } + @Test + public void testEntityOfArrays(SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final EntityPersister entityDescriptor = sessionFactory.getMappingMetamodel() + .findEntityDescriptor( EntityOfArrays.class ); + + scope.inTransaction( session -> { + final var entity = session.createSelectionQuery( + "from EntityOfArrays e where id = 1", + EntityOfArrays.class + ).getSingleResult(); + + try { + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); + visitor.visit( entityDescriptor.getEntityMappingType(), entity, sessionFactory.getWrapperOptions(), writer ); + final String result = writer.toString(); + + final JsonNode jsonNode = mapper.readTree( result ); + System.out.println("Result: "+jsonNode.toPrettyString()); + + assertJsonArray( jsonNode.get( "integerArray" ), Arrays.asList( 1, null, 3 ) ); + assertJsonArray( jsonNode.get( "stringCollectionArray" ), List.of( "one", "two", "three" ) ); + assertJsonArray( jsonNode.get( "longArray" ), List.of( 10L, 20L, 30L ) ); + assertJsonArray( jsonNode.get( "booleanListArray" ), List.of( true, false, true ) ); + } + catch (Exception e) { + fail( "Test failed with exception", e ); + } + } ); + } + + private static void assertJsonArray(JsonNode jsonNode, List expectedValues) { + assertThat( jsonNode.isArray() ).isTrue(); + assertThat( jsonNode.size() ).isEqualTo( expectedValues.size() ); + for (int i = 0; i < expectedValues.size(); i++) { + final Object expectedValue = expectedValues.get( i ); + final JsonNode element = jsonNode.get( i ); + if ( expectedValue == null ) { + assertThat( element.isNull() ).isTrue(); + } + else if ( expectedValue instanceof Boolean ) { + assertThat( element.booleanValue() ).isEqualTo( expectedValue ); + } + else if ( expectedValue instanceof Integer ) { + assertThat( element.intValue() ).isEqualTo( expectedValue ); + } + else if ( expectedValue instanceof Long ) { + assertThat( element.longValue() ).isEqualTo( expectedValue ); + } + else if ( expectedValue instanceof String ) { + assertThat( element.textValue() ).isEqualTo( expectedValue ); + } + else { + fail( "Unsupported element type: " + expectedValue.getClass() ); + } + } + } + @BeforeAll public void beforeAll(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -122,6 +188,13 @@ public void beforeAll(SessionFactoryScope scope) { session.persist( rh ); session.persist( new Employee( UUID.randomUUID(), "Marco", "Belladelli", 100_000, rh ) ); session.persist( new Employee( UUID.randomUUID(), "Matteo", "Cauzzi", 50_000, rh ) ); + final var ofArrays = new EntityOfArrays(); + ofArrays.id = 1L; + ofArrays.integerArray = new Integer[] { 1, null, 3 }; + ofArrays.stringCollectionArray = List.of( "one", "two", "three" ); + ofArrays.longArray = new long[] { 10, 20, 30 }; + ofArrays.booleanListArray = List.of( true, false, true ); + session.persist( ofArrays ); } ); } @@ -276,4 +349,22 @@ public void setCompany(Company company) { this.company = company; } } + + @Entity(name = "EntityOfArrays") + static class EntityOfArrays { + @Id + private Long id; + + @JdbcTypeCode(SqlTypes.ARRAY) + private Integer[] integerArray; + + @JdbcTypeCode(SqlTypes.ARRAY) + private Collection stringCollectionArray; + + @JdbcTypeCode(SqlTypes.JSON_ARRAY) + private long[] longArray; + + @JdbcTypeCode(SqlTypes.JSON_ARRAY) + private List booleanListArray; + } } From fe1996963b5ee0bbcdc23f644248d9a22b0e7a54 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Thu, 6 Nov 2025 12:27:03 +0100 Subject: [PATCH 2/2] HHH-19906 JSON serialization: handle non-array values for array types --- .../jdbc/spi/JsonGeneratingVisitor.java | 118 +++++++++++++----- 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java index f84111f1884c..32ed497b8a21 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JsonGeneratingVisitor.java @@ -15,7 +15,6 @@ import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.type.BasicType; -import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; @@ -27,6 +26,7 @@ import java.io.IOException; import java.lang.reflect.Array; +import java.util.Collection; import static org.hibernate.type.descriptor.jdbc.StructHelper.getSubPart; @@ -37,7 +37,8 @@ public class JsonGeneratingVisitor { public static final JsonGeneratingVisitor INSTANCE = new JsonGeneratingVisitor(); - protected JsonGeneratingVisitor() {} + protected JsonGeneratingVisitor() { + } /** * Serializes an array of values into JSON object/array @@ -56,40 +57,82 @@ public void visitArray(JavaType elementJavaType, JdbcType elementJdbcType, Ob } if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) { - final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); - for ( Object value : values ) { + visitPluralAggregateValues( jsonElementJdbcType, values, options, writer ); + } + else { + assert !(elementJdbcType instanceof AggregateJdbcType); + visitBasicPluralValues( elementJavaType, elementJdbcType, values, options, writer ); + } + writer.endArray(); + } + + private void visitPluralAggregateValues( + AggregateJdbcType elementJdbcType, + Object values, + WrapperOptions options, + JsonDocumentWriter writer) { + final EmbeddableMappingType embeddableMappingType = elementJdbcType.getEmbeddableMappingType(); + if ( values.getClass().isArray() ) { + final var length = Array.getLength( values ); + for ( int j = 0; j < length; j++ ) { + try { + visit( embeddableMappingType, Array.get( values, j ), options, writer ); + } + catch (IOException e) { + throw new IllegalArgumentException( "Could not serialize array element", e ); + } + } + } + else if ( values instanceof Collection collection ) { + for ( final var item : collection ) { try { - visit( embeddableMappingType, value, options, writer ); + visit( embeddableMappingType, item, options, writer ); } catch (IOException e) { - throw new IllegalArgumentException( "Could not serialize JSON array value", e ); + throw new IllegalArgumentException( "Could not serialize array element", e ); } } } else { - assert !(elementJdbcType instanceof AggregateJdbcType); - for ( Object value : values ) { - if ( value == null ) { + throw new IllegalArgumentException( + "Expected array or collection value, but got: " + values.getClass().getName() + ); + } + } + + private void visitBasicPluralValues( + JavaType elementJavaType, + JdbcType elementJdbcType, + Object values, + WrapperOptions options, + JsonDocumentWriter writer) { + if ( values.getClass().isArray() ) { + final var length = Array.getLength( values ); + for ( int j = 0; j < length; j++ ) { + final var item = Array.get( values, j ); + if ( item == null ) { writer.nullValue(); } else { - writer.serializeJsonValue( value, (JavaType) elementJavaType, elementJdbcType, options ); + writer.serializeJsonValue( item, elementJavaType, elementJdbcType, options ); } } } - - writer.endArray(); - } - - /** - * Checks that a JDBCType is assignable to an array - * - * @param type the jdbc type - * @return true if types is of array kind false otherwise. - */ - private static boolean isArrayType(JdbcType type) { - return (type.getDefaultSqlTypeCode() == SqlTypes.ARRAY || - type.getDefaultSqlTypeCode() == SqlTypes.JSON_ARRAY); + else if ( values instanceof Collection collection ) { + for ( final var item : collection ) { + if ( item == null ) { + writer.nullValue(); + } + else { + writer.serializeJsonValue( item, elementJavaType, elementJdbcType, options ); + } + } + } + else { + throw new IllegalArgumentException( + "Expected array or collection value, but got: " + values.getClass().getName() + ); + } } public void visit(MappingType mappedType, Object value, WrapperOptions options, JsonDocumentWriter writer) @@ -106,17 +149,26 @@ else if ( mappedType instanceof ManagedMappingType managedMappingType ) { serializeObject( managedMappingType, value, options, writer ); } else if ( mappedType instanceof BasicType basicType ) { - if ( isArrayType( basicType.getJdbcType() ) ) { - final int length = Array.getLength( value ); + if ( basicType.getJdbcType() instanceof ArrayJdbcType arrayJdbcType ) { + final var domainValue = basicType.convertToRelationalValue( value ); + final var elementJdbcType = arrayJdbcType.getElementJdbcType(); writer.startArray(); - if ( length != 0 ) { - //noinspection unchecked - final JavaType elementJavaType = ((BasicPluralJavaType) basicType.getJdbcJavaType()).getElementJavaType(); - final var elementJdbcType = ((ArrayJdbcType) basicType.getJdbcType()).getElementJdbcType(); - final Object domainArray = basicType.convertToRelationalValue( value ); - for ( int j = 0; j < length; j++ ) { - writer.serializeJsonValue( Array.get( domainArray, j ), elementJavaType, elementJdbcType, options ); - } + if ( elementJdbcType instanceof AggregateJdbcType aggregateJdbcType ) { + visitPluralAggregateValues( + aggregateJdbcType, + domainValue, + options, + writer + ); + } + else { + visitBasicPluralValues( + ((BasicPluralJavaType) basicType.getJdbcJavaType()).getElementJavaType(), + elementJdbcType, + domainValue, + options, + writer + ); } writer.endArray(); }