From fff05630f9700d65be16d6b40fe0ef3dada370fa Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Mon, 10 Aug 2026 20:36:52 -0500 Subject: [PATCH] feat(postgresql): Add json/jsonb type support to PostgresqlDataTypeFactory PostgresqlDataTypeFactory had no recognition for PostgreSQL's native json/jsonb columns (reported as sql type OTHER), so they fell through to DefaultDataTypeFactory with no dedicated read/write handling. Add JsonType, following the same reflection-based PGobject approach as UuidType/InetType/CitextType so dbUnit doesn't need a compile-time dependency on the PostgreSQL driver. One instance handles a single sql type name ("json" or "jsonb") since PostgreSQL has no implicit cast between the two; PostgresqlDataTypeFactory constructs the matching instance per column automatically. * Unlike the issue reporter's original 2015 patch attachment, which a prior maintainer reply already flagged as NPEing from setSqlValue() on a null value, this implementation explicitly null-checks in both setSqlValue() (binds sql NULL) and typeCast() (returns null). * Add JsonTypeTest (9 cases, including a Mockito-based regression test for the null setSqlValue() path) and 2 new PostgresqlDataTypeFactoryTest cases. * Add PostgresqlJsonIT, round-tripping json and jsonb columns (including a null row) through a live PostgreSQL 16 container; jsonb assertions compare parsed document structure since PostgreSQL reformats jsonb text on storage. * Document JsonType in databases/postgresql.adoc, cross-linking IsActualEqualToExpectedJsonValueComparer for semantic JSON comparison. Refs: 574 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017rt6YcFdhBCwafLp6n7mcZ --- src/changes/changes.xml | 3 + .../org/dbunit/ext/postgresql/JsonType.java | 151 ++++++++++++++++++ .../postgresql/PostgresqlDataTypeFactory.java | 3 + src/site/asciidoc/databases/postgresql.adoc | 34 +++- .../dbunit/ext/postgresql/JsonTypeTest.java | 123 ++++++++++++++ .../PostgresqlDataTypeFactoryTest.java | 34 ++++ .../ext/postgresql/PostgresqlJsonIT.java | 149 +++++++++++++++++ 7 files changed, 494 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/dbunit/ext/postgresql/JsonType.java create mode 100644 src/test/java/org/dbunit/ext/postgresql/JsonTypeTest.java create mode 100644 src/test/java/org/dbunit/ext/postgresql/PostgresqlJsonIT.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 49b525309..60d2f79e0 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -264,6 +264,9 @@ Fix DatabaseDataSet#getTable()/getTableMetaData() throwing NoSuchTableException for every table in a schema the very first time that schema is accessed under a different case than the database itself stored it in, when FEATURE_QUALIFIED_TABLE_NAMES is enabled (issue 656: a multi-schema PostgreSQL FlatXmlDataSet row like "CORE.USER" against a live "core" schema - PostgreSQL folds an unquoted schema name to lower case at creation time). DatabaseDataSet#initialize() passed the requested schema straight into IMetadataHandler#getTables(), whose underlying DatabaseMetaData#getTables() matches the schema pattern case-sensitively against the live catalog regardless of dbUnit's own FEATURE_CASE_SENSITIVE_TABLE_NAMES setting, so a wrongly-cased first request found zero tables - and DatabaseDataSet's own per-schema initialization cache then permanently remembered that schema as already-initialized-but-empty, so even a later, correctly-cased retry kept failing too. This also explains the original report's second observation, that later rows loaded fine once some earlier row happened to use the schema's actual case: that merely primed the cache under a casing OrderedTableNameMap's existing case-insensitive table lookup could then match. DatabaseDataSet now resolves a requested schema name against DatabaseMetaData#getSchemas()'s actual reported names, case-insensitively, before querying for tables, whenever FEATURE_CASE_SENSITIVE_TABLE_NAMES is off (the default); a schema that doesn't exist under any casing still resolves to zero tables exactly as before. + + Add JsonType, recognized by PostgresqlDataTypeFactory for `json` and `jsonb` columns (reported as sql type OTHER), read/written as their raw text representation via the same reflection-based PGobject approach as UuidType/InetType/CitextType, so a bound parameter's PGobject type name always matches its target column exactly - PostgreSQL has no implicit cast between `json` and `jsonb`. Unlike the reporter's original 2015 patch attachment, setSqlValue() explicitly handles a null value (binding sql NULL) instead of dereferencing it, and typeCast() returns null for a null input. + diff --git a/src/main/java/org/dbunit/ext/postgresql/JsonType.java b/src/main/java/org/dbunit/ext/postgresql/JsonType.java new file mode 100644 index 000000000..b1f8a75ca --- /dev/null +++ b/src/main/java/org/dbunit/ext/postgresql/JsonType.java @@ -0,0 +1,151 @@ +/* + * + * The DbUnit Database Testing Framework + * Copyright (C)2002-2026, DbUnit.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package org.dbunit.ext.postgresql; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Objects; + +import org.dbunit.dataset.datatype.AbstractDataType; +import org.dbunit.dataset.datatype.TypeCastException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Adapter to handle conversion between PostgreSQL native {@code json}/ + * {@code jsonb} types and Strings. + * + *

PostgreSQL requires a bound parameter's declared type to match its + * target {@code json}/{@code jsonb} column exactly - it has no implicit cast + * between the two - so one instance handles only the single sql type name it + * was constructed with. {@link PostgresqlDataTypeFactory} constructs the + * matching instance for each column automatically; there is normally no need + * to instantiate this class directly. + * + *

This class only shuttles the column's raw text between the driver and + * the dataset; it does not compare JSON values semantically. Use + * {@link org.dbunit.assertion.comparer.value.IsActualEqualToExpectedJsonValueComparer} + * to compare json/jsonb column values by their parsed document structure + * instead of raw text. + * + * @author Jeff Jensen + * @since 3.4.1 + */ +public class JsonType extends AbstractDataType +{ + private static final Logger logger = + LoggerFactory.getLogger(JsonType.class); + + private final String sqlTypeName; + + /** + * Creates a data type adapter for the given PostgreSQL JSON sql type + * name. + * + * @param sqlTypeName + * The sql type name, {@code "json"} or {@code "jsonb"}, + * needed to invoke the {@code setType()} method on the + * PGobject class. + */ + public JsonType(final String sqlTypeName) + { + super(Objects.requireNonNull(sqlTypeName, + "The parameter 'sqlTypeName' must not be null"), Types.OTHER, + String.class, false); + + this.sqlTypeName = sqlTypeName; + } + + @Override + public Object getSqlValue(final int column, final ResultSet resultSet) + throws SQLException, TypeCastException + { + return resultSet.getString(column); + } + + @Override + public void setSqlValue(final Object value, final int column, + final PreparedStatement statement) + throws SQLException, TypeCastException + { + if (value == null) + { + statement.setNull(column, Types.OTHER); + return; + } + + statement.setObject(column, + getJson(value, statement.getConnection())); + } + + @Override + public Object typeCast(final Object value) throws TypeCastException + { + return value == null ? null : value.toString(); + } + + /** + * Returns the sql type name this instance handles. + * + * @return {@code "json"} or {@code "jsonb"}, whichever sql type name this + * instance was constructed with. + */ + public String getSqlTypeName() + { + return sqlTypeName; + } + + private Object getJson(final Object value, final Connection connection) + throws TypeCastException + { + logger.debug("getJson(value={}, connection={}) - start", value, + connection); + + Object tempJson = null; + + try + { + final Class aPGObjectClass = super.loadClass( + "org.postgresql.util.PGobject", connection); + final Constructor ct = aPGObjectClass.getConstructor(); + tempJson = ct.newInstance(); + + final Method setTypeMethod = + aPGObjectClass.getMethod("setType", String.class); + setTypeMethod.invoke(tempJson, sqlTypeName); + + final Method setValueMethod = + aPGObjectClass.getMethod("setValue", String.class); + setValueMethod.invoke(tempJson, value.toString()); + + } catch (final ReflectiveOperationException e) + { + throw new TypeCastException(value, this, e); + } + + return tempJson; + } +} diff --git a/src/main/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.java b/src/main/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.java index 07f2d3b44..4831eb9fb 100644 --- a/src/main/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.java +++ b/src/main/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.java @@ -98,6 +98,9 @@ public DataType createDataType(final int sqlType, final String sqlTypeName) } else if ("citext".equals(sqlTypeName)) { return new CitextType(); + } else if ("json".equals(sqlTypeName) || "jsonb".equals(sqlTypeName)) + { + return new JsonType(sqlTypeName); } else { // Finally check whether the user defined a custom datatype diff --git a/src/site/asciidoc/databases/postgresql.adoc b/src/site/asciidoc/databases/postgresql.adoc index 5e2bc9ecf..7b77a3a0a 100644 --- a/src/site/asciidoc/databases/postgresql.adoc +++ b/src/site/asciidoc/databases/postgresql.adoc @@ -9,9 +9,10 @@ equivalent. == IDataTypeFactory link:/dbunit/apidocs/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.html[PostgresqlDataTypeFactory] -recognizes `uuid`, `interval`, `inet`, `geometry`, `citext`, `oid`, and -(via an overridable hook) custom enum types, mapping each to the dedicated -classes below; everything else delegates to `DefaultDataTypeFactory`. +recognizes `uuid`, `interval`, `inet`, `geometry`, `citext`, `json`, `jsonb`, +`oid` when reported as JDBC `BIGINT`, and (via an overridable hook) custom +enum types, mapping each to the dedicated classes below; everything else +delegates to `DefaultDataTypeFactory`. Register it via `DatabaseConfig.PROPERTY_DATATYPE_FACTORY` — see link:../properties.html#typefactory[Properties] and link:../connections.html[Connections & Configuration]. @@ -45,6 +46,7 @@ connection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, |link:/dbunit/apidocs/org/dbunit/ext/postgresql/InetType.html[InetType] |`inet` (IP address/network) values. |link:/dbunit/apidocs/org/dbunit/ext/postgresql/UuidType.html[UuidType] |`uuid` values. |link:/dbunit/apidocs/org/dbunit/ext/postgresql/GeometryType.html[GeometryType] |`geometry` (PostGIS) values, read/written as their string representation. +|link:/dbunit/apidocs/org/dbunit/ext/postgresql/JsonType.html[JsonType] |`json` and `jsonb` values, read/written as their raw text representation. |=== === GenericEnumType @@ -69,7 +71,33 @@ See the FAQ for more: link:../faq.html#postgresqlEnumTypes[Are Postgresql enum types supported by dbunit?] +=== JsonType + +link:/dbunit/apidocs/org/dbunit/ext/postgresql/JsonType.html[JsonType] +adapts between PostgreSQL's native `json`/`jsonb` types and Strings, using +the same reflection-based `PGobject.setType()` approach as the other types +above. PostgreSQL has no implicit cast between `json` and `jsonb`, so a +bound parameter's PGobject type must match its target column exactly; +`PostgresqlDataTypeFactory` handles this automatically by constructing a +`JsonType` for the specific sql type name (`json` or `jsonb`) each column +reports. + +This class only shuttles a column's raw text between the driver and the +dataset; it does not compare JSON values semantically. PostgreSQL +reformats `jsonb` text on storage (for example, inserting whitespace or +reordering object keys), so a literal string comparison against an +expected dataset value can spuriously fail even when the JSON is +semantically identical. Use +link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.html[IsActualEqualToExpectedJsonValueComparer] +(see link:../datacomparisons/valuecomparer.html[Value Comparers]) to compare +`json`/`jsonb` column values by their parsed document structure instead. + == Known Quirks Enum type support requires the `isEnumType()` override above — see GenericEnumType. + +`json`/`jsonb` column comparisons in assertions are literal string +comparisons unless you use +link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.html[IsActualEqualToExpectedJsonValueComparer] +— see JsonType above. diff --git a/src/test/java/org/dbunit/ext/postgresql/JsonTypeTest.java b/src/test/java/org/dbunit/ext/postgresql/JsonTypeTest.java new file mode 100644 index 000000000..ad011a2e3 --- /dev/null +++ b/src/test/java/org/dbunit/ext/postgresql/JsonTypeTest.java @@ -0,0 +1,123 @@ +/* + * + * The DbUnit Database Testing Framework + * Copyright (C)2002-2004, DbUnit.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package org.dbunit.ext.postgresql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; + +import java.sql.PreparedStatement; +import java.sql.Types; + +import org.dbunit.dataset.datatype.AbstractDataType; +import org.dbunit.dataset.datatype.TypeCastException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Unit tests for {@link JsonType}. + * + * @author Jeff Jensen + * @since 3.4.1 + */ +@ExtendWith(MockitoExtension.class) +class JsonTypeTest extends AbstractPostgresqlStringDataTypeTest +{ + @Mock + private PreparedStatement statement; + + @Override + protected AbstractDataType createType() + { + return new JsonType("json"); + } + + @Test + void testConstructor_withValidSqlTypeName_storesSqlTypeName() + { + final JsonType type = new JsonType("jsonb"); + assertThat(type.getSqlTypeName()) + .as("getSqlTypeName() should return the name passed to the constructor.") + .isEqualTo("jsonb"); + } + + @Test + void testConstructor_withNullSqlTypeName_throwsNullPointerException() + { + assertThatThrownBy(() -> new JsonType(null)) + .as("Constructor should throw NullPointerException when sqlTypeName is null.") + .isInstanceOf(NullPointerException.class); + } + + @Test + void testTypeCast_withJsonObjectString_returnsStringRepresentation() + throws TypeCastException + { + final JsonType type = new JsonType("json"); + final String value = "{\"a\":1}"; + final Object result = type.typeCast(value); + assertThat(result) + .as("typeCast() should return the string representation of the value.") + .isEqualTo("{\"a\":1}"); + } + + @Test + void testTypeCast_withJsonArrayString_returnsStringRepresentation() + throws TypeCastException + { + final JsonType type = new JsonType("jsonb"); + final String value = "[1,2,3]"; + final Object result = type.typeCast(value); + assertThat(result) + .as("typeCast() should return the string representation of the value.") + .isEqualTo("[1,2,3]"); + } + + @Test + void testTypeCast_withNullValue_returnsNull() throws TypeCastException + { + final JsonType type = new JsonType("json"); + final Object result = type.typeCast(null); + assertThat(result) + .as("typeCast() should return null when given null.") + .isNull(); + } + + /** + * Issue 574: the reporter's original patch threw NPE from setSqlValue() + * when the column value was null. + */ + @Test + void testSetSqlValue_withNullValue_doesNotThrowAndSetsSqlNull() + throws Exception + { + final JsonType type = new JsonType("jsonb"); + + assertThatCode(() -> type.setSqlValue(null, 1, statement)) + .as("setSqlValue() should not throw NullPointerException when value is null.") + .doesNotThrowAnyException(); + + verify(statement).setNull(1, Types.OTHER); + } +} diff --git a/src/test/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactoryTest.java b/src/test/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactoryTest.java index e855dbfbc..267ab5875 100644 --- a/src/test/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactoryTest.java +++ b/src/test/java/org/dbunit/ext/postgresql/PostgresqlDataTypeFactoryTest.java @@ -96,6 +96,40 @@ void testCreateCitextType_withCitextTypeName_returnsCitextTypeInstance() throws assertThat(result).isInstanceOf(CitextType.class); } + @Test + void testCreateJsonType_withJsonTypeName_returnsJsonTypeInstance() throws Exception + { + final PostgresqlDataTypeFactory instance = + new PostgresqlDataTypeFactory(); + + final int sqlType = Types.OTHER; + final String sqlTypeName = "json"; + + final DataType result = instance.createDataType(sqlType, sqlTypeName); + assertThat(result).as("createDataType() should return a JsonType for json.") + .isInstanceOf(JsonType.class); + assertThat(((JsonType) result).getSqlTypeName()) + .as("The JsonType should keep the json sql type name.") + .isEqualTo("json"); + } + + @Test + void testCreateJsonType_withJsonbTypeName_returnsJsonTypeInstance() throws Exception + { + final PostgresqlDataTypeFactory instance = + new PostgresqlDataTypeFactory(); + + final int sqlType = Types.OTHER; + final String sqlTypeName = "jsonb"; + + final DataType result = instance.createDataType(sqlType, sqlTypeName); + assertThat(result).as("createDataType() should return a JsonType for jsonb.") + .isInstanceOf(JsonType.class); + assertThat(((JsonType) result).getSqlTypeName()) + .as("The JsonType should keep the jsonb sql type name.") + .isEqualTo("jsonb"); + } + @Test void testCreateEnumType_withCustomEnumTypeName_returnsGenericEnumTypeInstance() throws Exception { diff --git a/src/test/java/org/dbunit/ext/postgresql/PostgresqlJsonIT.java b/src/test/java/org/dbunit/ext/postgresql/PostgresqlJsonIT.java new file mode 100644 index 000000000..b832f0614 --- /dev/null +++ b/src/test/java/org/dbunit/ext/postgresql/PostgresqlJsonIT.java @@ -0,0 +1,149 @@ +package org.dbunit.ext.postgresql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.StringReader; +import java.sql.Statement; +import java.sql.Types; +import java.util.Objects; + +import org.dbunit.DatabaseEnvironment; +import org.dbunit.database.DatabaseConfig; +import org.dbunit.database.IDatabaseConnection; +import org.dbunit.dataset.Column; +import org.dbunit.dataset.IDataSet; +import org.dbunit.dataset.ITable; +import org.dbunit.dataset.ITableMetaData; +import org.dbunit.dataset.ReplacementDataSet; +import org.dbunit.dataset.xml.FlatXmlDataSetBuilder; +import org.dbunit.operation.DatabaseOperation; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.xml.sax.InputSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Integration test proving {@link JsonType} round-trips PostgreSQL + * {@code json}/{@code jsonb} column values, including a null value, through + * a real database (issue 574). + * + * @author Jeff Jensen + * @since 3.4.1 + */ +@EnabledIfSystemProperty(named = "dbunit.profile", matches = "postgresql") +class PostgresqlJsonIT +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private IDatabaseConnection _connection; + private final String testTable = "json_test"; + // @formatter:off + private static final String xmlData = "" + + "" + + "" + + "" + + ""; + // @formatter:on + + @BeforeEach + protected void setUp() throws Exception + { + // Load active postgreSQL profile and connection from Maven pom.xml. + _connection = DatabaseEnvironment.getInstance().getConnection(); + try (Statement stat = _connection.getConnection().createStatement()) + { + stat.execute("DROP TABLE IF EXISTS " + testTable + ";"); + stat.execute("CREATE TABLE " + testTable + + "(ID INTEGER NOT NULL, DATA json, DATA_B jsonb);"); + } + // Mirrors PostgresqlUuidIT: the table isn't visible to a fresh + // dataset without reopening the connection. + _connection.close(); + _connection = DatabaseEnvironment.getInstance().getConnection(); + } + + @AfterEach + protected void tearDown() throws Exception + { + if (!Objects.isNull(_connection)) + { + try (Statement stat = + _connection.getConnection().createStatement()) + { + stat.execute("DROP TABLE IF EXISTS " + testTable + ";"); + } finally + { + _connection.close(); + _connection = null; + } + } + } + + @Test + void testJsonDataType_withJsonAndJsonbColumns_roundTripsThroughDatabase() + throws Exception + { + assertThat(_connection).as("didn't get a connection.").isNotNull(); + final DatabaseConfig config = _connection.getConfig(); + config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, + new PostgresqlDataTypeFactory()); + + final ReplacementDataSet dataSet = + new ReplacementDataSet(new FlatXmlDataSetBuilder() + .build(new InputSource(new StringReader(xmlData)))); + dataSet.addReplacementObject("[NULL]", null); + dataSet.setStrictReplacement(true); + + final IDataSet metaDataSet = _connection.createDataSet(); + final ITableMetaData itmd = metaDataSet.getTableMetaData(testTable); + boolean dataChecked = false; + boolean dataBChecked = false; + for (final Column col : itmd.getColumns()) + { + if ("DATA".equalsIgnoreCase(col.getColumnName())) + { + dataChecked = true; + assertThat(col.getDataType().getSqlType()) + .as("DATA column sql type.").isEqualTo(Types.OTHER); + assertThat(col.getSqlTypeName()).as("DATA column sql type name.") + .isEqualTo("json"); + } else if ("DATA_B".equalsIgnoreCase(col.getColumnName())) + { + dataBChecked = true; + assertThat(col.getDataType().getSqlType()) + .as("DATA_B column sql type.").isEqualTo(Types.OTHER); + assertThat(col.getSqlTypeName()).as("DATA_B column sql type name.") + .isEqualTo("jsonb"); + } + } + assertThat(dataChecked).as("The DATA column should be present in the metadata.") + .isTrue(); + assertThat(dataBChecked).as("The DATA_B column should be present in the metadata.") + .isTrue(); + + DatabaseOperation.CLEAN_INSERT.execute(_connection, dataSet); + + final IDataSet actualDataSet = _connection.createDataSet(); + final ITable actualTable = actualDataSet.getTable(testTable); + + // PostgreSQL stores "json" verbatim but reformats "jsonb" on write + // (e.g. inserted whitespace), so compare parsed document structure + // rather than raw text. + assertThat(OBJECT_MAPPER.readTree( + String.valueOf(actualTable.getValue(0, "DATA")))) + .as("DATA (json) row 0 should round-trip the same document.") + .isEqualTo(OBJECT_MAPPER.readTree("{\"a\":1}")); + assertThat(OBJECT_MAPPER.readTree( + String.valueOf(actualTable.getValue(0, "DATA_B")))) + .as("DATA_B (jsonb) row 0 should round-trip the same document.") + .isEqualTo(OBJECT_MAPPER.readTree("{\"b\":2}")); + + assertThat(actualTable.getValue(1, "DATA")) + .as("DATA (json) row 1 should be null.").isNull(); + assertThat(actualTable.getValue(1, "DATA_B")) + .as("DATA_B (jsonb) row 1 should be null.").isNull(); + } +}