Merge pull request #567 from aperaverzeu/4.3-FEATURE-CAY-provide-ability-to-add-extra-modules-in-declarative-way

Provide ability to add extra modules in declarative way
diff --git a/.github/workflows/verify-deploy-on-push.yml b/.github/workflows/verify-deploy-on-push.yml
index dabe35d..a891b5c 100644
--- a/.github/workflows/verify-deploy-on-push.yml
+++ b/.github/workflows/verify-deploy-on-push.yml
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: verify and deploy
+name: verify and deploy 5.0
 
 on: push
 
diff --git a/.github/workflows/verify-on-pr.yml b/.github/workflows/verify-on-pr.yml
index 79390a4..bf0e8de 100644
--- a/.github/workflows/verify-on-pr.yml
+++ b/.github/workflows/verify-on-pr.yml
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-name: verify PR
+name: verify PR 5.0
 
 on: pull_request
 
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 4d2d670..77fdda9 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -36,6 +36,8 @@
 CAY-2781 Enable 'Create PK properties' by default in the cgen configuration
 CAY-2784 Remove 'Use Java primitive types' option from dbimport
 CAY-2788 DbImport: Add fallback option for the batch attribute loader
+CAY-2795 Add unit tests for the Json type
+CAY-2802 Upgrade Gradle to 7.6.1
 
 Bug Fixes:
 
@@ -49,4 +51,5 @@
 CAY-2774 Overriding service ordering in DI List causes DIRuntimeException
 CAY-2782 Modeler: save button becomes active on DataMap comment field focus
 CAY-2783 DbEntity to ObjEntity synchronization should check mandatory flag for primitive java types
-CAY-2792 Fix Insertion Order For Reflexive DataObjects
\ No newline at end of file
+CAY-2792 Fix Insertion Order For Reflexive DataObjects
+CAY-2801 Incorrect equals() implementation in IdGenerationMarker could cause data missing in the commit
\ No newline at end of file
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
index 5f769c9..3bff928 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table1.java
@@ -8,11 +8,11 @@
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table1;
 import org.apache.cayenne.crypto.db.Table7;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 
 /**
@@ -25,7 +25,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table1> SELF = PropertyFactory.createSelf(Table1.class);
+    public static final SelfProperty<Table1> SELF = PropertyFactory.createSelf(Table1.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table1", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
index 7ccd3e5..f8f0272 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table2.java
@@ -7,9 +7,9 @@
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table2;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 
 /**
  * Class _Table2 was generated by Cayenne.
@@ -21,7 +21,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table2> SELF = PropertyFactory.createSelf(Table2.class);
+    public static final SelfProperty<Table2> SELF = PropertyFactory.createSelf(Table2.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table2", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
index bc9e695..c01e383 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table3.java
@@ -6,9 +6,9 @@
 
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table3;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 
 /**
@@ -21,7 +21,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table3> SELF = PropertyFactory.createSelf(Table3.class);
+    public static final SelfProperty<Table3> SELF = PropertyFactory.createSelf(Table3.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table3", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
index 862d110..591f439 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table4.java
@@ -6,10 +6,10 @@
 
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table4;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 
 /**
@@ -22,7 +22,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table4> SELF = PropertyFactory.createSelf(Table4.class);
+    public static final SelfProperty<Table4> SELF = PropertyFactory.createSelf(Table4.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table4", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
index f5cd4d8..b12bd98 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table5.java
@@ -6,10 +6,10 @@
 
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table5;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 
 /**
  * Class _Table5 was generated by Cayenne.
@@ -21,7 +21,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table5> SELF = PropertyFactory.createSelf(Table5.class);
+    public static final SelfProperty<Table5> SELF = PropertyFactory.createSelf(Table5.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table5", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
index 9404fd8..b925ff7 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table6.java
@@ -6,10 +6,10 @@
 
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table6;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 
 /**
  * Class _Table6 was generated by Cayenne.
@@ -21,7 +21,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table6> SELF = PropertyFactory.createSelf(Table6.class);
+    public static final SelfProperty<Table6> SELF = PropertyFactory.createSelf(Table6.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table6", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table7.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table7.java
index ef0cc9e..11141ab 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table7.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table7.java
@@ -11,6 +11,7 @@
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 
 /**
@@ -23,7 +24,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table7> SELF = PropertyFactory.createSelf(Table7.class);
+    public static final SelfProperty<Table7> SELF = PropertyFactory.createSelf(Table7.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table7", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table8.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table8.java
index 1003a6f..cc632f9 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table8.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/auto/_Table8.java
@@ -6,9 +6,9 @@
 
 import org.apache.cayenne.BaseDataObject;
 import org.apache.cayenne.crypto.db.Table8;
-import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericIdProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 
 /**
@@ -21,7 +21,7 @@
 
     private static final long serialVersionUID = 1L;
 
-    public static final EntityProperty<Table8> SELF = PropertyFactory.createSelf(Table8.class);
+    public static final SelfProperty<Table8> SELF = PropertyFactory.createSelf(Table8.class);
 
     public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "Table8", Integer.class);
     public static final String ID_PK_COLUMN = "ID";
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoaderIT.java
index d811e48..378de73 100644
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoaderIT.java
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoaderIT.java
@@ -44,8 +44,9 @@
         DbEntity artist = getDbEntity("ARTIST");
         DbAttribute a = getDbAttribute(artist, "ARTIST_ID");
         assertNotNull(a);
-        if(accessStackAdapter.onlyGenericNumberType()) {
-            assertEquals(Types.INTEGER, a.getType());
+        if (accessStackAdapter.onlyGenericNumberType()) {
+            // All integer types are mapped to NUMERIC in Oracle DB.
+            assertEquals(Types.NUMERIC, a.getType());
         } else {
             assertEquals(Types.BIGINT, a.getType());
         }
@@ -105,15 +106,27 @@
         // check varchar
         assertEquals(msgForTypeMismatch(Types.VARCHAR, varcharAttr), Types.VARCHAR, varcharAttr.getType());
         assertEquals(255, varcharAttr.getMaxLength());
+
         // check integer
-        assertEquals(msgForTypeMismatch(Types.INTEGER, integerAttr), Types.INTEGER, integerAttr.getType());
+        // All integer types are mapped to NUMERIC in Oracle DB.
+        if (accessStackAdapter.onlyGenericNumberType()) {
+            assertEquals(msgForTypeMismatch(Types.NUMERIC, integerAttr), Types.NUMERIC, integerAttr.getType());
+        } else {
+            assertEquals(msgForTypeMismatch(Types.INTEGER, integerAttr), Types.INTEGER, integerAttr.getType());
+        }
+
         // check float
         assertTrue(msgForTypeMismatch(Types.FLOAT, floatAttr), Types.FLOAT == floatAttr.getType()
                 || Types.DOUBLE == floatAttr.getType() || Types.REAL == floatAttr.getType());
 
         // check smallint
-        assertTrue(msgForTypeMismatch(Types.SMALLINT, smallintAttr), Types.SMALLINT == smallintAttr.getType()
-                || Types.INTEGER == smallintAttr.getType());
+        // All integer types are mapped to NUMERIC in Oracle DB.
+        if (accessStackAdapter.onlyGenericNumberType()) {
+            assertEquals(msgForTypeMismatch(Types.NUMERIC, smallintAttr), Types.NUMERIC, smallintAttr.getType());
+        } else {
+            assertTrue(msgForTypeMismatch(Types.SMALLINT, smallintAttr),
+                       Types.SMALLINT == smallintAttr.getType() || Types.INTEGER == smallintAttr.getType());
+        }
     }
 
     private void assertGenerated() {
diff --git a/cayenne-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/cayenne-gradle-plugin/gradle/wrapper/gradle-wrapper.properties
index 1c1126a..edd4bfc 100644
--- a/cayenne-gradle-plugin/gradle/wrapper/gradle-wrapper.properties
+++ b/cayenne-gradle-plugin/gradle/wrapper/gradle-wrapper.properties
@@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java
index 299e352..5aead02 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java
@@ -31,10 +31,8 @@
 class IdGenerationMarker implements Serializable, InternalUnsupportedTypeFactory.Marker {
     private static final long serialVersionUID = -5339942931435878094L;
 
-    private final static IdGenerationMarker INSTANCE = new IdGenerationMarker();
-
     static IdGenerationMarker marker() {
-        return INSTANCE;
+        return new IdGenerationMarker();
     }
 
     private IdGenerationMarker() {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
index 1f48871..eae298b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
@@ -35,6 +35,7 @@
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
@@ -78,10 +79,13 @@
         super.configureExtendedTypes(map);
 
         // create specially configured CharType handler
-        map.registerType(new CharType(true, true));
+        CharType charType = new CharType(true, true);
+        map.registerType(charType);
         // configure boolean type to work with numeric columns
         map.registerType(new DB2BooleanType());
+
         map.registerType(new ByteArrayType(false, false));
+        map.registerType(new JsonType(charType, true));
     }
 
     /**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
index ea13e66..e598202 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
@@ -26,6 +26,7 @@
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
@@ -36,6 +37,7 @@
 import org.apache.cayenne.query.SQLAction;
 import org.apache.cayenne.resource.ResourceLocator;
 
+import java.sql.Types;
 import java.util.List;
 
 /**
@@ -106,6 +108,19 @@
         super.configureExtendedTypes(map);
 
         // create specially configured CharType handler
-        map.registerType(new H2CharType());
+        H2CharType charType = new H2CharType();
+        map.registerType(charType);
+
+        map.registerType(new JsonType(charType, false));
     }
+
+    @Override
+    public DbAttribute buildAttribute(String name, String typeName, int type, int size, int scale, boolean allowNulls) {
+        if ("json".equalsIgnoreCase(typeName)) {
+            type = Types.OTHER;
+        }
+        return super.buildAttribute(name, typeName, type, size, scale, allowNulls);
+    }
+
+
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
index a2273c0..b19c685 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
@@ -28,6 +28,7 @@
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
@@ -50,7 +51,7 @@
  * DbAdapter implementation for the <a href="http://hsqldb.sourceforge.net/">
  * HSQLDB RDBMS </a>. Sample connection settings to use with HSQLDB are shown
  * below:
- * 
+ *
  * <pre>
  *        test-hsqldb.jdbc.username = test
  *        test-hsqldb.jdbc.password = secret
@@ -80,7 +81,10 @@
 		super.configureExtendedTypes(map);
 
 		// create specially configured CharType handler
-		map.registerType(new CharType(true, true));
+		CharType charType = new CharType(true, true);
+		map.registerType(charType);
+
+		map.registerType(new JsonType(charType, true));
 	}
 
 	/**
@@ -104,7 +108,7 @@
 	/**
 	 * Generate fully-qualified name for 1.8 and on. Subclass generates
 	 * unqualified name.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	protected String getTableName(DbEntity entity) {
@@ -114,7 +118,7 @@
 	/**
 	 * Returns DbEntity schema name for 1.8 and on. Subclass generates
 	 * unqualified name.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	protected String getSchemaName(DbEntity entity) {
@@ -123,7 +127,7 @@
 
 	/**
 	 * Uses special action builder to create the right action.
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	@Override
@@ -133,7 +137,7 @@
 
 	/**
 	 * Returns a DDL string to create a unique constraint over a set of columns.
-	 * 
+	 *
 	 * @since 1.1
 	 */
 	@Override
@@ -171,7 +175,7 @@
 
 	/**
 	 * Adds an ADD CONSTRAINT clause to a relationship constraint.
-	 * 
+	 *
 	 * @see JdbcAdapter#createFkConstraint(DbRelationship)
 	 */
 	@Override
@@ -224,7 +228,7 @@
 
 	/**
 	 * Uses "CREATE CACHED TABLE" instead of "CREATE TABLE".
-	 * 
+	 *
 	 * @since 1.2
 	 */
 	@Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
index 2f2b7b3..276fbe8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
@@ -36,9 +36,11 @@
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.ByteType;
+import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.ShortType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
@@ -200,7 +202,8 @@
 		super.configureExtendedTypes(map);
 
 		// create specially configured CharType handler
-		map.registerType(new OracleCharType());
+		OracleCharType charType = new OracleCharType();
+		map.registerType(charType);
 
 		// create specially configured ByteArrayType handler
 		map.registerType(new OracleByteArrayType());
@@ -211,7 +214,9 @@
 		// At least on MacOS X, driver does not handle Short and Byte properly
 		map.registerType(new ShortType(true));
 		map.registerType(new ByteType(true));
+
 		map.registerType(new OracleBooleanType());
+		map.registerType(new JsonType(charType, true));
 	}
 
 	/**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
index d1373a0..465433b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
@@ -116,10 +116,13 @@
 	@Override
 	public DbAttribute buildAttribute(String name, String typeName, int type, int size, int scale, boolean allowNulls) {
 
+		if ("json".equalsIgnoreCase(typeName)) {
+			type = Types.OTHER;
+		}
 		// "bytea" maps to pretty much any binary type, so
 		// it is up to us to select the most sensible default.
 		// And the winner is LONGVARBINARY
-		if (BYTEA.equalsIgnoreCase(typeName)) {
+		else if (BYTEA.equalsIgnoreCase(typeName)) {
 			type = Types.LONGVARBINARY;
 		}
 		// oid is returned as INTEGER, need to make it BLOB
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
index e191bc8..b3b4484 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
@@ -24,8 +24,11 @@
 
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
+import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
+import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.RuntimeProperties;
@@ -120,6 +123,15 @@
 		return new SQLServerTreeProcessor();
 	}
 
+	@Override
+	protected void configureExtendedTypes(ExtendedTypeMap map) {
+		super.configureExtendedTypes(map);
+
+		CharType charType = new CharType(true, false);
+		map.registerType(charType);
+		map.registerType(new JsonType(charType, true));
+	}
+
 	/**
 	 * Uses SQLServerActionBuilder to create the right action.
 	 *
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java b/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java
index d824b41..309ce55 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java
@@ -19,20 +19,31 @@
 
 package org.apache.cayenne.value;
 
+import java.io.Serializable;
 import java.util.Objects;
 
 import org.apache.cayenne.value.json.JsonUtils;
+import org.apache.cayenne.value.json.MalformedJsonException;
 
 /**
  * A Cayenne-supported values object that holds Json string.
  *
  * @since 4.2
  */
-public class Json {
+public class Json implements Serializable {
 
+    private static final long serialVersionUID = 7594825997288498022L;
     private final String json;
 
+    /**
+     *
+     * @param json json string representation
+     * @throws MalformedJsonException if json is empty or blank
+     */
     public Json(String json) {
+        if (json.isBlank()) {
+            throw new MalformedJsonException("Unexpected EOF");
+        }
         this.json = json;
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/json/AbstractJsonConsumer.java b/cayenne-server/src/main/java/org/apache/cayenne/value/json/AbstractJsonConsumer.java
index d364868..254213c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/value/json/AbstractJsonConsumer.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/json/AbstractJsonConsumer.java
@@ -52,8 +52,11 @@
     protected abstract T output();
 
     T process() {
-        JsonTokenizer.JsonToken token;
-        while((token = tokenizer.nextToken()).getType() != JsonTokenizer.TokenType.NONE) {
+        JsonTokenizer.JsonToken token = tokenizer.nextToken();
+        if (token.getType() == JsonTokenizer.TokenType.NONE) {
+            throw new MalformedJsonException("Unexpected EOF");
+        }
+        do {
             switch (token.getType()) {
                 case ARRAY_START:
                     onArrayStart();
@@ -79,7 +82,7 @@
                     processValue(token);
                     break;
             }
-        }
+        } while ((token = tokenizer.nextToken()).getType() != JsonTokenizer.TokenType.NONE);
         return output();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonFormatter.java b/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonFormatter.java
index 3d697a1..384143f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonFormatter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonFormatter.java
@@ -41,6 +41,10 @@
             builder.delete(builder.length() - 2, builder.length());
         }
         builder.append(']');
+        if (State.OBJECT_VALUE.equals(currentState())) {
+            setState(State.OBJECT_KEY);
+            builder.append(", ");
+        }
     }
 
     @Override
@@ -54,6 +58,9 @@
             builder.delete(builder.length() - 2, builder.length());
         }
         builder.append('}');
+        if (State.ARRAY.equals(currentState())) {
+            builder.append(", ");
+        }
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonUtils.java b/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonUtils.java
index bbcb0cf..4d407d6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonUtils.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/json/JsonUtils.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.value.json;
 
+import java.util.Objects;
+
 /**
  * Simple utils to process JSON.
  *
@@ -53,7 +55,7 @@
     public static boolean compare(String json1, String json2) {
         Object object1 = new JsonReader(json1).process();
         Object object2 = new JsonReader(json2).process();
-        return object1.equals(object2);
+        return Objects.equals(object1, object2);
     }
 
     private JsonUtils() {
diff --git a/cayenne-server/src/main/resources/org/apache/cayenne/dba/oracle/types.xml b/cayenne-server/src/main/resources/org/apache/cayenne/dba/oracle/types.xml
index 96432c1..d17ab2b 100644
--- a/cayenne-server/src/main/resources/org/apache/cayenne/dba/oracle/types.xml
+++ b/cayenne-server/src/main/resources/org/apache/cayenne/dba/oracle/types.xml
@@ -106,7 +106,7 @@
        <db-type name="RAW"/>
    </jdbc-type>
    <jdbc-type name="VARCHAR">
-       <db-type name="VARCHAR"/>
+       <db-type name="VARCHAR2"/>
    </jdbc-type>
    <jdbc-type name="NVARCHAR">
        <db-type name="NVARCHAR2"/>
diff --git a/cayenne-server/src/main/resources/org/apache/cayenne/dba/postgres/types.xml b/cayenne-server/src/main/resources/org/apache/cayenne/dba/postgres/types.xml
index 7b8db93..bb886aa 100644
--- a/cayenne-server/src/main/resources/org/apache/cayenne/dba/postgres/types.xml
+++ b/cayenne-server/src/main/resources/org/apache/cayenne/dba/postgres/types.xml
@@ -84,7 +84,9 @@
    <jdbc-type name="NUMERIC">
        <db-type name="numeric"/>
    </jdbc-type>
-   <jdbc-type name="OTHER"/>
+   <jdbc-type name="OTHER">
+       <db-type name="json"/>
+   </jdbc-type>
    <jdbc-type name="REAL">
        <db-type name="real"/>
    </jdbc-type>
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
index 6cfc68f..8d21b73 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java
@@ -22,6 +22,8 @@
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.testdo.relationships.E1;
 import org.apache.cayenne.testdo.relationships.E2;
+import org.apache.cayenne.unit.OracleUnitDbAdapter;
+import org.apache.cayenne.unit.UnitDbAdapter;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -33,8 +35,11 @@
 public class CircularDependencyIT extends ServerCase {
 
     @Inject
+    private UnitDbAdapter unitDbAdapter;
+
+    @Inject
     private ObjectContext context;
-    
+
     @Test()
     public void testCycle() {
         E1 e1 = context.newObject(E1.class);
@@ -50,8 +55,15 @@
             context.commitChanges();
             fail("Exception should be thrown here");
         } catch (CayenneRuntimeException ex) {
-            assertTrue("Unexpected exception message: " + ex.getMessage(),
-                    ex.getMessage().contains("PK is not generated"));
+            // TODO: Oracle adapter still does not fully support key generation.
+            if (unitDbAdapter instanceof OracleUnitDbAdapter) {
+                assertTrue(ex.getCause().getMessage().contains("parent key not found"));
+            } else {
+                assertTrue(String.format("Unexpected exception message: %s%nCause: %s - %s",
+                                ex.getMessage(), ex.getCause(),
+                                ex.getCause() != null ? ex.getCause().getMessage() : null),
+                        ex.getMessage().contains("PK is not generated"));
+            }
         }
 
     }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641IT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641IT.java
index 98d51b8..aeb062d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641IT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641IT.java
@@ -35,6 +35,8 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Before;
 import org.junit.Test;
+import org.testcontainers.shaded.org.hamcrest.MatcherAssert;
+import org.testcontainers.shaded.org.hamcrest.Matchers;
 
 import java.sql.Types;
 import java.util.List;
@@ -78,8 +80,8 @@
         String sql = translator.getSql();
         assertFalse(sql.contains("t0.NAME"));
 
-        String string = "SELECT t0.SURNAME, t0.ID FROM ArtistLazy t0";
-        assertEquals(sql, string);
+        String pattern = "SELECT t0.SURNAME( c0)?, t0.ID( c1)? FROM ArtistLazy t0";
+        MatcherAssert.assertThat(sql, Matchers.matchesPattern(pattern));
 
         ColumnSelect<String> select = ObjectSelect.columnQuery(ArtistLazy.class, ArtistLazy.NAME);
         translator = new DefaultSelectTranslator(select, adapter, context.getEntityResolver());
@@ -118,8 +120,9 @@
         String sql = translator.getSql();
         assertFalse(sql.contains("t0.NAME"));
 
-        String string = "SELECT t0.ARTIST_ID, t0.ID, t1.ID, t1.SURNAME FROM PaintingLazy t0 LEFT JOIN ArtistLazy t1 ON t0.ARTIST_ID = t1.ID";
-        assertEquals(sql, string);
+        String pattern = "SELECT t0.ARTIST_ID( c0)?, t0.ID( c1)?, t1.ID( c2)?, t1.SURNAME( c3)?"
+                + " FROM PaintingLazy t0 LEFT JOIN ArtistLazy t1 ON t0.ARTIST_ID = t1.ID";
+        MatcherAssert.assertThat(sql, Matchers.matchesPattern(pattern));
     }
 
     @Test
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
index 4ec42b6..ee04557 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
@@ -509,7 +509,7 @@
             ObjectContext context1 = runtime.newContext();
             CompoundPainting o1 = context1.newObject(CompoundPainting.class);
             o1.setArtistName("A1");
-            o1.setEstimatedPrice(new BigDecimal(1d));
+            o1.setEstimatedPrice(BigDecimal.valueOf(1));
             o1.setGalleryName("G1");
             o1.setPaintingTitle("P1");
             o1.setTextReview("T1");
@@ -524,7 +524,7 @@
             CompoundPainting o2 = SelectById.query(CompoundPainting.class, id).selectFirst(context2);
 
             o2.setArtistName("AX1");
-            o2.setEstimatedPrice(new BigDecimal(2d));
+            o2.setEstimatedPrice(BigDecimal.valueOf(2));
             o2.setGalleryName("XG1");
             o2.setPaintingTitle("PX1");
             o2.setTextReview("TX1");
@@ -538,7 +538,7 @@
             CompoundPainting o3 = SelectById.query(CompoundPainting.class, id).selectFirst(context3);
 
             assertEquals("AX1", o3.getArtistName());
-            assertEquals("2.00", o3.getEstimatedPrice().toPlainString());
+            assertEquals(0, BigDecimal.valueOf(2).compareTo(o3.getEstimatedPrice()));
             assertEquals("XG1", o3.getGalleryName());
             assertEquals("PX1", o3.getPaintingTitle());
             assertEquals("TX1", o3.getTextReview());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
index 3d4398d..f15d316 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
@@ -143,29 +143,29 @@
     public void testBigDecimal_Decimal() {
 
         // this matches the column scale exactly
-        String v1 = "7890.123456";
+        BigDecimal v1 = new BigDecimal("7890.123456");
         // this has lower scale than the column
-        String v2 = "7890.1";
-        String v2_padded = "7890.100000";
+        BigDecimal v2 = new BigDecimal("7890.1");
+        BigDecimal v2_padded = new BigDecimal("7890.100000");
 
         BigDecimalEntity o = context.newObject(BigDecimalEntity.class);
-        o.setBigDecimalDecimal(new BigDecimal(v1));
+        o.setBigDecimalDecimal(v1);
         o.getObjectContext().commitChanges();
         assertEquals(1, commitStats.getCommitCount());
         BigDecimalEntity o1 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v1, o1.getBigDecimalDecimal().toString());
+        assertEquals(0, v1.compareTo(o1.getBigDecimalDecimal()));
 
-        o.setBigDecimalDecimal(new BigDecimal(v2));
+        o.setBigDecimalDecimal(v2);
         o.getObjectContext().commitChanges();
         BigDecimalEntity o2 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v2_padded, o2.getBigDecimalDecimal().toString());
+        assertEquals(0, v2_padded.compareTo(o2.getBigDecimalDecimal()));
         assertEquals(2, commitStats.getCommitCount());
 
-        o2.setBigDecimalDecimal(new BigDecimal(v2));
+        o2.setBigDecimalDecimal(v2);
         o2.getObjectContext().commitChanges();
         assertEquals("Commit was not expected. The difference is purely in value padding", 2, commitStats.getCommitCount());
         BigDecimalEntity o3 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v2_padded, o3.getBigDecimalDecimal().toString());
+        assertEquals(0, v2_padded.compareTo(o3.getBigDecimalDecimal()));
 
         o3.setBigDecimalDecimal(null);
         o3.getObjectContext().commitChanges();
@@ -177,27 +177,27 @@
     @Test
     public void testBigDecimal_Numeric() {
 
-        String v1 = "1234567890.44";
-        String v2 = "1234567890.4";
-        String v2_padded = "1234567890.40";
+        BigDecimal v1 = new BigDecimal("1234567890.44");
+        BigDecimal v2 = new BigDecimal("1234567890.4");
+        BigDecimal v2_padded = new BigDecimal("1234567890.40");
 
         BigDecimalEntity o = context.newObject(BigDecimalEntity.class);
-        o.setBigDecimalNumeric(new BigDecimal(v1));
+        o.setBigDecimalNumeric(v1);
         o.getObjectContext().commitChanges();
         assertEquals(1, commitStats.getCommitCount());
         BigDecimalEntity o1 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v1, o1.getBigDecimalNumeric().toString());
+        assertEquals(0, v1.compareTo(o1.getBigDecimalNumeric()));
 
-        o1.setBigDecimalNumeric(new BigDecimal(v2));
+        o1.setBigDecimalNumeric(v2);
         o1.getObjectContext().commitChanges();
         assertEquals(2, commitStats.getCommitCount());
         BigDecimalEntity o2 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v2_padded, o2.getBigDecimalNumeric().toString());
+        assertEquals(0, v2_padded.compareTo(o2.getBigDecimalNumeric()));
 
-        o2.setBigDecimalNumeric(new BigDecimal(v2));
+        o2.setBigDecimalNumeric(v2);
         assertEquals("Commit was not expected. The difference is purely in value padding", 2, commitStats.getCommitCount());
         BigDecimalEntity o3 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
-        assertEquals(v2_padded, o3.getBigDecimalNumeric().toString());
+        assertEquals(0, v2_padded.compareTo(o3.getBigDecimalNumeric()));
 
         o3.setBigDecimalNumeric(null);
         o3.getObjectContext().commitChanges();
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java
index 4877e0c..d9a7dbc 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java
@@ -40,6 +40,7 @@
         EffectiveOpId effectiveOpId2 = new EffectiveOpId("test", Collections.singletonMap("pk", ObjectIdValueSupplier.getFor(id1, "pk")));
 
         assertEquals(effectiveOpId1, effectiveOpId2);
+        assertNotEquals(id1.getReplacementIdMap().get("pk"), IdGenerationMarker.marker());
     }
 
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java
index 8a2c3b8..fae99a8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java
@@ -111,17 +111,17 @@
 
         Expression maxExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).max().getExpression();
 
-        Object max0 = maxExp.evaluate(artists.get(0));
+        BigDecimal max0 = (BigDecimal) maxExp.evaluate(artists.get(0));
         BigDecimal expected0 = BigDecimal.valueOf(20000, 2);
-        assertEquals(expected0, max0);
+        assertEquals(0, expected0.compareTo(max0));
 
-        Object max1 = maxExp.evaluate(artists.get(1));
+        BigDecimal max1 = (BigDecimal) maxExp.evaluate(artists.get(1));
         BigDecimal expected1 = BigDecimal.valueOf(100000, 2);
-        assertEquals(expected1, max1);
+        assertEquals(0, expected1.compareTo(max1));
 
-        Object max4 = maxExp.evaluate(artists.get(4));
+        BigDecimal max4 = (BigDecimal) maxExp.evaluate(artists.get(4));
         BigDecimal expected4 = BigDecimal.valueOf(19000, 2);
-        assertEquals(expected4, max4);
+        assertEquals(0, expected4.compareTo(max4));
     }
 
     @Test
@@ -133,17 +133,17 @@
 
         Expression minExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).min().getExpression();
 
-        Object min0 = minExp.evaluate(artists.get(0));
+        BigDecimal min0 = (BigDecimal) minExp.evaluate(artists.get(0));
         BigDecimal expected0 = BigDecimal.valueOf(5000, 2);
-        assertEquals(expected0, min0);
+        assertEquals(0, expected0.compareTo(min0));
 
-        Object min3 = minExp.evaluate(artists.get(3));
+        BigDecimal min3 = (BigDecimal) minExp.evaluate(artists.get(3));
         BigDecimal expected1 = BigDecimal.valueOf(3000, 2);
-        assertEquals(expected1, min3);
+        assertEquals(0, expected1.compareTo(min3));
 
-        Object min4 = minExp.evaluate(artists.get(4));
+        BigDecimal min4 = (BigDecimal) minExp.evaluate(artists.get(4));
         BigDecimal expected4 = BigDecimal.valueOf(4000, 2);
-        assertEquals(expected4, min4);
+        assertEquals(0, expected4.compareTo(min4));
     }
 
     @Test
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
index e738c75..e91ce8d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTFunctionCallStringIT.java
@@ -27,6 +27,8 @@
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.unit.OracleUnitDbAdapter;
+import org.apache.cayenne.unit.UnitDbAdapter;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -34,6 +36,7 @@
 
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertNull;
+import static org.junit.Assume.assumeFalse;
 
 /**
  * @since 4.0
@@ -42,6 +45,9 @@
 public class ASTFunctionCallStringIT extends ServerCase {
 
     @Inject
+    private UnitDbAdapter unitDbAdapter;
+
+    @Inject
     private ObjectContext context;
 
     private Artist createArtist(String name) throws Exception {
@@ -62,6 +68,10 @@
 
     @Test
     public void testASTUpperInWhere() throws Exception {
+        // TODO: This will fail for Oracle, so skip for now.
+        //       It is necessary to provide connection with "fixedString=true" property somehow.
+        //       Also see CAY-1470.
+        assumeFalse(unitDbAdapter instanceof OracleUnitDbAdapter);
         Artist a1 = createArtist("name");
         Artist a2 = ObjectSelect.query(Artist.class)
                 .where(Artist.ARTIST_NAME.upper().eq("NAME")).selectOne(context);
@@ -70,6 +80,10 @@
 
     @Test
     public void testASTLowerInWhere() throws Exception {
+        // TODO: This will fail for Oracle, so skip for now.
+        //       It is necessary to provide connection with "fixedString=true" property somehow.
+        //       Also see CAY-1470.
+        assumeFalse(unitDbAdapter instanceof OracleUnitDbAdapter);
         Artist a1 = createArtist("NAME");
         Artist a2 = ObjectSelect.query(Artist.class)
                 .where(Artist.ARTIST_NAME.lower().eq("name")).selectOne(context);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
index fe6e25a..b4b5554 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java
@@ -25,11 +25,14 @@
 import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.dba.oracle.OracleAdapter;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.OracleUnitDbAdapter;
+import org.apache.cayenne.unit.UnitDbAdapter;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -38,6 +41,7 @@
 
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -46,6 +50,9 @@
 public class ObjectSelect_RunIT extends ServerCase {
 
 	@Inject
+	private UnitDbAdapter unitDbAdapter;
+
+	@Inject
 	private DataContext context;
 
 	@Inject
@@ -199,6 +206,10 @@
 
 	@Test
 	public void test_Select_CustomFunction() {
+		// TODO: This will fail for Oracle, so skip for now.
+		//       It is necessary to provide connection with "fixedString=true" property somehow.
+		//       Also see CAY-1470.
+		assumeFalse(unitDbAdapter instanceof OracleUnitDbAdapter);
 		Artist a = ObjectSelect.query(Artist.class)
 				.where(Artist.ARTIST_NAME.function("UPPER", String.class).eq("ARTIST1"))
 				.selectOne(context);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
index f1b44a0..2617c0c 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLTemplateIT.java
@@ -31,6 +31,7 @@
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.testdo.testmap.Gallery;
 import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.OracleUnitDbAdapter;
 import org.apache.cayenne.unit.UnitDbAdapter;
 import org.apache.cayenne.unit.di.DataChannelInterceptor;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
@@ -39,6 +40,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.math.BigDecimal;
 import java.sql.Date;
 import java.sql.SQLException;
 import java.time.LocalDateTime;
@@ -311,7 +313,10 @@
 		assertEquals(2, artists.size());
 		assertEquals(1, artists.get(0).length);
 		assertTrue(artists.get(0) instanceof Object[]);
-		assertTrue(artists.get(0)[0] instanceof Long);
+
+        // TODO: JDBC's BIGINT matches Oracle's NUMERIC, which matches BigDecimal.
+        Class<?> idType = unitDbAdapter instanceof OracleUnitDbAdapter ? BigDecimal.class : Long.class;
+        assertThat(artists.get(0)[0], instanceOf(idType));
 	}
 
 	@Test
@@ -325,7 +330,10 @@
 		assertEquals(2, artists.size());
 		assertEquals(2, artists.get(0).length);
 		assertTrue(artists.get(0) instanceof Object[]);
-		assertTrue(artists.get(0)[0] instanceof Long);
+
+        // JDBC's BIGINT matches Oracle's NUMERIC, which matches BigDecimal.
+        Class<?> idType = unitDbAdapter instanceof OracleUnitDbAdapter ? BigDecimal.class : Long.class;
+		assertThat(artists.get(0)[0], instanceOf(idType));
 	}
 
 	@Test
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonOther.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonOther.java
new file mode 100644
index 0000000..7a5fd7d
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonOther.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.testdo.json;
+
+import org.apache.cayenne.testdo.json.auto._JsonOther;
+
+public class JsonOther extends _JsonOther {
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonVarchar.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonVarchar.java
new file mode 100644
index 0000000..8c8ac93
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/JsonVarchar.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.testdo.json;
+
+import org.apache.cayenne.testdo.json.auto._JsonVarchar;
+
+public class JsonVarchar extends _JsonVarchar {
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonOther.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonOther.java
new file mode 100644
index 0000000..651123f
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonOther.java
@@ -0,0 +1,113 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.testdo.json.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.exp.property.NumericIdProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.testdo.json.JsonOther;
+import org.apache.cayenne.value.Json;
+
+/**
+ * Class _JsonOther was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _JsonOther extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final SelfProperty<JsonOther> SELF = PropertyFactory.createSelf(JsonOther.class);
+
+    public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "JsonOther", Integer.class);
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final BaseProperty<Json> DATA = PropertyFactory.createBase("data", Json.class);
+
+    protected Json data;
+
+
+    public void setData(Json data) {
+        beforePropertyWrite("data", this.data, data);
+        this.data = data;
+    }
+
+    public Json getData() {
+        beforePropertyRead("data");
+        return this.data;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "data":
+                return this.data;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "data":
+                this.data = (Json)val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.data);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.data = (Json)in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonVarchar.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonVarchar.java
new file mode 100644
index 0000000..8a3ade2
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/json/auto/_JsonVarchar.java
@@ -0,0 +1,113 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.testdo.json.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.exp.property.NumericIdProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.testdo.json.JsonVarchar;
+import org.apache.cayenne.value.Json;
+
+/**
+ * Class _JsonVarchar was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _JsonVarchar extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final SelfProperty<JsonVarchar> SELF = PropertyFactory.createSelf(JsonVarchar.class);
+
+    public static final NumericIdProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumericId("ID", "JsonVarchar", Integer.class);
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final BaseProperty<Json> DATA = PropertyFactory.createBase("data", Json.class);
+
+    protected Json data;
+
+
+    public void setData(Json data) {
+        beforePropertyWrite("data", this.data, data);
+        this.data = data;
+    }
+
+    public Json getData() {
+        beforePropertyRead("data");
+        return this.data;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "data":
+                return this.data;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "data":
+                this.data = (Json)val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.data);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.data = (Json)in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
index b5222ad..aed1805 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/PostgresUnitDbAdapter.java
@@ -67,6 +67,11 @@
     }
 
     @Override
+    public boolean supportsJsonType() {
+        return true;
+    }
+
+    @Override
     public boolean supportsGeneratedKeysDrop() {
         return true;
     }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
index 87fa7bd..7122d4b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
@@ -222,6 +222,13 @@
         return supportsLobs();
     }
 
+    /**
+     * Returns true if the target database has native json data type.
+     */
+    public boolean supportsJsonType() {
+        return false;
+    }
+
     public boolean supportsBinaryPK() {
         return true;
     }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index ccb392e..f99577f 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -41,6 +41,7 @@
     public static final String INHERITANCE_PROJECT = "cayenne-inheritance.xml";
     public static final String INHERITANCE_SINGLE_TABLE1_PROJECT = "cayenne-inheritance-single-table1.xml";
     public static final String INHERITANCE_VERTICAL_PROJECT = "cayenne-inheritance-vertical.xml";
+    public static final String JSON_PROJECT = "cayenne-json.xml";
     public static final String LIFECYCLE_CALLBACKS_ORDER_PROJECT = "cayenne-lifecycle-callbacks-order.xml";
     public static final String LIFECYCLES_PROJECT = "cayenne-lifecycles.xml";
     public static final String LOB_PROJECT = "cayenne-lob.xml";
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index bc154c6..d89054b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -51,11 +51,12 @@
 import java.sql.Statement;
 import java.sql.Types;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 
 /**
@@ -69,25 +70,28 @@
 
 	private static final String[] MAPS_REQUIRING_SCHEMA_SETUP = { "testmap.map.xml", "compound.map.xml",
 			"misc-types.map.xml", "things.map.xml", "numeric-types.map.xml", "binary-pk.map.xml", "no-pk.map.xml",
-			"lob.map.xml", "date-time.map.xml", "enum.map.xml", "extended-type.map.xml", "generated.map.xml",
-			"mixed-persistence-strategy.map.xml", "people.map.xml", "primitive.map.xml", "inheritance.map.xml",
-			"locking.map.xml", "soft-delete.map.xml", "empty.map.xml", "relationships.map.xml",
+			"lob.map.xml", "date-time.map.xml", "enum.map.xml", "json.map.xml", "extended-type.map.xml",
+			"generated.map.xml", "mixed-persistence-strategy.map.xml", "people.map.xml", "primitive.map.xml",
+			"inheritance.map.xml", "locking.map.xml", "soft-delete.map.xml", "empty.map.xml", "relationships.map.xml",
 			"relationships-activity.map.xml", "relationships-delete-rules.map.xml",
 			"relationships-collection-to-many.map.xml", "relationships-child-master.map.xml",
-			"relationships-clob.map.xml", "relationships-flattened.map.xml", "relationships-many-to-many-join.map.xml", "relationships-set-to-many.map.xml",
-			"relationships-to-many-fk.map.xml", "relationships-to-one-fk.map.xml", "return-types.map.xml",
-			"uuid.map.xml", "multi-tier.map.xml", "reflexive.map.xml", "delete-rules.map.xml",
-            "lifecycle-callbacks-order.map.xml", "lifecycles.map.xml", "map-to-many.map.xml", "toone.map.xml", "meaningful-pk.map.xml",
-			"table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml", "map-db2.map.xml", "embeddable.map.xml",
-			"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
-			"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
-			"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml", "hybrid-data-object.map.xml",
-			"java8.map.xml", "inheritance-with-enum.map.xml", "lazy-attributes.map.xml", "cay2666/datamap.map.xml", "cay2641/datamapLazy.map.xml",
-			"annotation/datamapAnnotation.map.xml" };
+			"relationships-clob.map.xml", "relationships-flattened.map.xml", "relationships-many-to-many-join.map.xml",
+			"relationships-set-to-many.map.xml", "relationships-to-many-fk.map.xml", "relationships-to-one-fk.map.xml",
+			"return-types.map.xml", "uuid.map.xml", "multi-tier.map.xml", "reflexive.map.xml", "delete-rules.map.xml",
+            "lifecycle-callbacks-order.map.xml", "lifecycles.map.xml", "map-to-many.map.xml", "toone.map.xml",
+            "meaningful-pk.map.xml", "table-primitives.map.xml", "generic.map.xml", "map-db1.map.xml",
+            "map-db2.map.xml", "embeddable.map.xml", "qualified.map.xml", "quoted-identifiers.map.xml",
+            "inheritance-single-table1.map.xml", "inheritance-vertical.map.xml", "oneway-rels.map.xml",
+            "unsupported-distinct-types.map.xml", "array-type.map.xml", "cay-2032.map.xml",
+            "weighted-sort.map.xml", "hybrid-data-object.map.xml", "java8.map.xml", "inheritance-with-enum.map.xml",
+            "lazy-attributes.map.xml", "cay2666/datamap.map.xml", "cay2641/datamapLazy.map.xml",
+            "annotation/datamapAnnotation.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported
-	private static final String[] EXTRA_EXCLUDED_FOR_NO_LOB = new String[] { "CLOB_DETAIL" };
+	private static final Set<String> EXTRA_EXCLUDED_FOR_NO_LOB = Set.of("CLOB_DETAIL");
+
+	private static final Set<String> EXTRA_EXCLUDED_FOR_NO_NATIVE_JSON = Set.of("JSON_OTHER");
 
 	private ServerCaseDataSourceFactory dataSourceFactory;
 	private UnitDbAdapter unitDbAdapter;
@@ -242,8 +246,8 @@
 	private List<DbEntity> dbEntitiesInInsertOrder(DataMap map) {
 		TreeMap<String, DbEntity> dbEntityMap = new TreeMap<>(map.getDbEntityMap());
 		List<DbEntity> entities = new ArrayList<>(dbEntityMap.values());
-
-		dbEntitiesFilter(entities);
+		List<DbEntity> excludedEntities = excludeEntities(entities);
+		entities.removeAll(excludedEntities);
 
 		domain.getEntitySorter().sortDbEntities(entities, false);
 		return entities;
@@ -253,70 +257,63 @@
 		DataMap map = domain.getDataMap(dataMap.getName());
 		Map<String, DbEntity> dbEntityMap = new TreeMap<>(map.getDbEntityMap());
 		List<DbEntity> entities = new ArrayList<>(dbEntityMap.values());
-
-		dbEntitiesFilter(entities);
+		List<DbEntity> excludedEntities = excludeEntities(entities);
+		entities.removeAll(excludedEntities);
 
 		domain.getEntitySorter().sortDbEntities(entities, true);
 		return entities;
 	}
 
-	// This seems actually unused for some time now (from 2014 to 2018), and caused no trouble
-	private void dbEntitiesFilter(List<DbEntity> entities) {
-		// filter various unsupported tests...
+	private List<DbEntity> excludeEntities(Collection<DbEntity> entities) {
+		// exclude various unsupported tests...
 
-		// LOBs
 		boolean excludeLOB = !unitDbAdapter.supportsLobs();
-		boolean excludeBinPK = !unitDbAdapter.supportsBinaryPK();
-		if (excludeLOB || excludeBinPK) {
+		boolean excludeNativeJson = !unitDbAdapter.supportsJsonType();
+		boolean excludeBinaryPK = !unitDbAdapter.supportsBinaryPK();
+		if (!excludeLOB && !excludeNativeJson && !excludeBinaryPK) {
+			return Collections.emptyList();
+		}
 
-			List<DbEntity> filtered = new ArrayList<>();
+		List<DbEntity> excludedEntities = new ArrayList<>();
+		for (DbEntity entity : entities) {
 
-			for (DbEntity ent : entities) {
-
-				// check for LOB attributes
-				if (excludeLOB) {
-					if (Arrays.binarySearch(EXTRA_EXCLUDED_FOR_NO_LOB, ent.getName()) >= 0) {
-						continue;
-					}
-
-					boolean hasLob = false;
-					for (final DbAttribute attr : ent.getAttributes()) {
-						if (attr.getType() == Types.BLOB || attr.getType() == Types.CLOB) {
-							hasLob = true;
-							break;
-						}
-					}
-
-					if (hasLob) {
-						continue;
-					}
+			// check for LOB attributes
+			if (excludeLOB) {
+				if (EXTRA_EXCLUDED_FOR_NO_LOB.contains(entity.getName())) {
+					excludedEntities.add(entity);
+					continue;
 				}
-
-				// check for BIN PK
-				if (excludeBinPK) {
-					boolean skip = false;
-					for (final DbAttribute attr : ent.getAttributes()) {
-						// check for BIN PK or FK to BIN Pk
-						if (attr.getType() == Types.BINARY || attr.getType() == Types.VARBINARY
-								|| attr.getType() == Types.LONGVARBINARY) {
-
-							if (attr.isPrimaryKey() || attr.isForeignKey()) {
-								skip = true;
-								break;
-							}
-						}
-					}
-
-					if (skip) {
-						continue;
-					}
+				Set<Integer> lobTypes = Set.of(Types.BLOB, Types.CLOB, Types.NCLOB);
+				boolean hasLob = entity.getAttributes().stream()
+						.map(DbAttribute::getType)
+						.anyMatch(lobTypes::contains);
+				if (hasLob) {
+					excludedEntities.add(entity);
+					continue;
 				}
-
-				filtered.add(ent);
 			}
 
-			entities = filtered;
+			// check for native json type
+			if (excludeNativeJson) {
+				if (EXTRA_EXCLUDED_FOR_NO_NATIVE_JSON.contains(entity.getName())) {
+					excludedEntities.add(entity);
+					continue;
+				}
+			}
+
+			// check for BIN PK
+			if (excludeBinaryPK) {
+				Set<Integer> binaryTypes = Set.of(Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY);
+				boolean hasBinaryPK = entity.getAttributes().stream()
+						.filter(attribute -> attribute.isPrimaryKey() || attribute.isForeignKey())
+						.map(DbAttribute::getType)
+						.anyMatch(binaryTypes::contains);
+				if (hasBinaryPK) {
+					excludedEntities.add(entity);
+				}
+			}
 		}
+		return excludedEntities;
 	}
 
 	private void dropSchema(DataNode node, DataMap map) throws Exception {
@@ -396,18 +393,19 @@
 	 */
 	private Collection<String> tableCreateQueries(DataNode node, DataMap map) {
 		DbAdapter adapter = node.getAdapter();
-		DbGenerator gen = new DbGenerator(adapter, map, null, domain, jdbcEventLogger);
 
-		List<DbEntity> orderedEnts = dbEntitiesInInsertOrder(map);
+		List<DbEntity> orderedEntities = dbEntitiesInInsertOrder(map);
+		List<DbEntity> excludedEntities = excludeEntities(map.getDbEntities());
+		DbGenerator gen = new DbGenerator(adapter, map, excludedEntities, domain, jdbcEventLogger);
 		List<String> queries = new ArrayList<>();
 
 		// table definitions
-		for (DbEntity ent : orderedEnts) {
+		for (DbEntity ent : orderedEntities) {
 			queries.add(adapter.createTable(ent));
 		}
 
 		// FK constraints
-		for (DbEntity ent : orderedEnts) {
+		for (DbEntity ent : orderedEntities) {
 			if (!unitDbAdapter.supportsFKConstraints(ent)) {
 				continue;
 			}
@@ -418,5 +416,4 @@
 
 		return queries;
 	}
-
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/testcontainers/OracleContainerProvider.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/testcontainers/OracleContainerProvider.java
index 6bdd2c4..4934c02 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/testcontainers/OracleContainerProvider.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/testcontainers/OracleContainerProvider.java
@@ -31,25 +31,18 @@
     @Override
     public JdbcDatabaseContainer<?> startContainer(String version) {
         JdbcDatabaseContainer<?> container = super.startContainer(version);
-        // need to wait to ensure Oracle DB has started
-        try {
-            Thread.sleep(40000);
-        } catch (InterruptedException ignored) {
-        }
         return container;
     }
 
     @Override
     JdbcDatabaseContainer<?> createContainer(DockerImageName dockerImageName) {
         return new OracleContainer(dockerImageName)
-                .withStartupTimeout(Duration.ofMinutes(5))
-                .withEnv("ORACLE_ALLOW_REMOTE", "true")
-                .withEnv("ORACLE_DISABLE_ASYNCH_IO", "true");
+                .withStartupTimeout(Duration.ofMinutes(5));
     }
 
     @Override
     String getDockerImage() {
-        return "oracleinanutshell/oracle-xe-11g";
+        return "gvenzl/oracle-xe:18-slim-faststart";
     }
 
     @Override
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonTypeIT.java b/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonTypeIT.java
new file mode 100644
index 0000000..a733df4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonTypeIT.java
@@ -0,0 +1,682 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.value.json;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.SelectById;
+import org.apache.cayenne.testdo.json.JsonOther;
+import org.apache.cayenne.testdo.json.JsonVarchar;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.apache.cayenne.value.Json;
+import org.junit.Test;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.JSON_PROJECT)
+public class JsonTypeIT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private UnitDbAdapter unitDbAdapter;
+
+    @Test
+    public void testJsonBasic() {
+        testJson("{\"id\": 1, \"property\": \"value\"}");
+    }
+
+    @Test
+    public void testJsonOver4k() {
+        testJson("[\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f151cd2df4280fe4258ef3\",\n" +
+                         "    \"index\": 0,\n" +
+                         "    \"guid\": \"2e05e933-7468-4573-aa76-e29a80810c57\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$3,282.00\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 20,\n" +
+                         "    \"eyeColor\": \"blue\",\n" +
+                         "    \"name\": \"Mathews Rutledge\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"FREAKIN\",\n" +
+                         "    \"email\": \"mathewsrutledge@freakin.com\",\n" +
+                         "    \"phone\": \"+1 (873) 411-3555\",\n" +
+                         "    \"address\": \"896 Lee Avenue, Fairhaven, New Jersey, 1031\",\n" +
+                         "    \"about\": \"nostrud cupidatat proident eu anim sint eu\",\n" +
+                         "    \"registered\": \"2021-10-10T11:59:50 -03:00\",\n" +
+                         "    \"latitude\": -18.854904,\n" +
+                         "    \"longitude\": -138.392089,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"ad\",\n" +
+                         "      \"irure\",\n" +
+                         "      \"anim\",\n" +
+                         "      \"aliqua\",\n" +
+                         "      \"do\",\n" +
+                         "      \"eu\",\n" +
+                         "      \"excepteur\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Guzman Kemp\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Delgado Beasley\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Noelle Owen\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Mathews Rutledge! You have 4 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f151cd00a687261825c3d3\",\n" +
+                         "    \"index\": 1,\n" +
+                         "    \"guid\": \"7c6b8859-5a81-4654-980f-e31ae7c3a04d\",\n" +
+                         "    \"isActive\": false,\n" +
+                         "    \"balance\": \"$2,805.03\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 32,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Nieves Gallegos\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"SPACEWAX\",\n" +
+                         "    \"email\": \"nievesgallegos@spacewax.com\",\n" +
+                         "    \"phone\": \"+1 (839) 414-3310\",\n" +
+                         "    \"address\": \"446 Mill Avenue, Nicholson, Minnesota, 8852\",\n" +
+                         "    \"about\": \"sunt magna quis officia exercitation laboris officia\",\n" +
+                         "    \"registered\": \"2020-01-17T08:49:38 -03:00\",\n" +
+                         "    \"latitude\": 11.681513,\n" +
+                         "    \"longitude\": -129.960233,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"labore\",\n" +
+                         "      \"fugiat\",\n" +
+                         "      \"cillum\",\n" +
+                         "      \"incididunt\",\n" +
+                         "      \"nostrud\",\n" +
+                         "      \"non\",\n" +
+                         "      \"et\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Perry Hunter\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Angelina Cooper\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Kendra Bonner\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Nieves Gallegos! You have 2 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"strawberry\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f151cd9d0e1b90e4150a10\",\n" +
+                         "    \"index\": 2,\n" +
+                         "    \"guid\": \"dfea2156-c940-43d3-a500-19b5b32719b3\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$2,684.11\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 38,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Virginia Watts\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"OPTIQUE\",\n" +
+                         "    \"email\": \"virginiawatts@optique.com\",\n" +
+                         "    \"phone\": \"+1 (864) 547-3451\",\n" +
+                         "    \"address\": \"518 Crooke Avenue, Advance, Arkansas, 7719\",\n" +
+                         "    \"about\": \"ullamco exercitation excepteur mollit ad labore do\",\n" +
+                         "    \"registered\": \"2018-11-30T07:01:17 -03:00\",\n" +
+                         "    \"latitude\": -77.530698,\n" +
+                         "    \"longitude\": 174.424542,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"ex\",\n" +
+                         "      \"ad\",\n" +
+                         "      \"exercitation\",\n" +
+                         "      \"dolor\",\n" +
+                         "      \"aute\",\n" +
+                         "      \"ex\",\n" +
+                         "      \"Lorem\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Molly Blake\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Pearlie Dodson\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Montoya Watkins\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Virginia Watts! You have 4 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"apple\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f151cde376bf473f79cc97\",\n" +
+                         "    \"index\": 3,\n" +
+                         "    \"guid\": \"4f77bba3-531c-4450-b441-589bd19f2a57\",\n" +
+                         "    \"isActive\": false,\n" +
+                         "    \"balance\": \"$3,381.01\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 22,\n" +
+                         "    \"eyeColor\": \"green\",\n" +
+                         "    \"name\": \"Walter Patrick\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"PEARLESEX\",\n" +
+                         "    \"email\": \"walterpatrick@pearlesex.com\",\n" +
+                         "    \"phone\": \"+1 (954) 448-3420\",\n" +
+                         "    \"address\": \"387 Church Avenue, Geyserville, New Hampshire, 4849\",\n" +
+                         "    \"about\": \"cupidatat officia qui dolor veniam eu minim\",\n" +
+                         "    \"registered\": \"2016-12-04T05:12:01 -03:00\",\n" +
+                         "    \"latitude\": 83.816972,\n" +
+                         "    \"longitude\": -30.59895,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"eu\",\n" +
+                         "      \"Lorem\",\n" +
+                         "      \"ad\",\n" +
+                         "      \"ea\",\n" +
+                         "      \"adipisicing\",\n" +
+                         "      \"velit\",\n" +
+                         "      \"ex\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Pate Sweet\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Stein Burns\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Candy Swanson\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Walter Patrick! You have 5 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f151cd64d59419599bd15f\",\n" +
+                         "    \"index\": 4,\n" +
+                         "    \"guid\": \"2da48623-9b34-47ec-962e-400d45c8620a\",\n" +
+                         "    \"isActive\": false,\n" +
+                         "    \"balance\": \"$2,891.52\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 37,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Ella Carey\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"PORTICO\",\n" +
+                         "    \"email\": \"ellacarey@portico.com\",\n" +
+                         "    \"phone\": \"+1 (906) 400-3097\",\n" +
+                         "    \"address\": \"381 Bowne Street, Rose, Palau, 7582\",\n" +
+                         "    \"about\": \"voluptate pariatur magna occaecat elit magna excepteur\",\n" +
+                         "    \"registered\": \"2015-08-07T10:22:10 -03:00\",\n" +
+                         "    \"latitude\": 80.548898,\n" +
+                         "    \"longitude\": 67.575077,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"duis\",\n" +
+                         "      \"occaecat\",\n" +
+                         "      \"excepteur\",\n" +
+                         "      \"tempor\",\n" +
+                         "      \"excepteur\",\n" +
+                         "      \"Lorem\",\n" +
+                         "      \"proident\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Melendez Martin\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Haley Colon\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Emilia Schmidt\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Ella Carey! You have 8 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"strawberry\"\n" +
+                         "  }\n" +
+                         "]");
+    }
+
+    @Test
+    public void testJsonOver16k() {
+        testJson("[\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f153352d42c0451f6f18a1\",\n" +
+                         "    \"index\": 0,\n" +
+                         "    \"guid\": \"4db9bc53-e652-47bd-8162-aad6b4280eb9\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$3,604.21\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 26,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Landry Malone\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"FROLIX\",\n" +
+                         "    \"email\": \"landrymalone@frolix.com\",\n" +
+                         "    \"phone\": \"+1 (898) 454-3351\",\n" +
+                         "    \"address\": \"881 Strauss Street, Troy, Utah, 4374\",\n" +
+                         "    \"about\": \"ea et consequat fugiat est laboris sint\",\n" +
+                         "    \"registered\": \"2016-06-14T05:08:16 -03:00\",\n" +
+                         "    \"latitude\": 49.131275,\n" +
+                         "    \"longitude\": -171.114829,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"dolore\",\n" +
+                         "      \"elit\",\n" +
+                         "      \"excepteur\",\n" +
+                         "      \"in\",\n" +
+                         "      \"sint\",\n" +
+                         "      \"exercitation\",\n" +
+                         "      \"cillum\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Palmer Pittman\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Clarice Wolfe\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Clements Battle\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Landry Malone! You have 9 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f153355ae6023dbfb7c2f8\",\n" +
+                         "    \"index\": 1,\n" +
+                         "    \"guid\": \"b4fdfc88-e367-42ba-ae1d-2f9f6a538986\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$1,909.36\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 24,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Nicholson Dodson\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"BUNGA\",\n" +
+                         "    \"email\": \"nicholsondodson@bunga.com\",\n" +
+                         "    \"phone\": \"+1 (857) 550-2984\",\n" +
+                         "    \"address\": \"425 Thornton Street, Roosevelt, South Carolina, 4979\",\n" +
+                         "    \"about\": \"voluptate consequat consequat pariatur reprehenderit et exercitation\",\n" +
+                         "    \"registered\": \"2021-08-16T06:28:03 -03:00\",\n" +
+                         "    \"latitude\": -12.667348,\n" +
+                         "    \"longitude\": 84.401994,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"est\",\n" +
+                         "      \"in\",\n" +
+                         "      \"mollit\",\n" +
+                         "      \"id\",\n" +
+                         "      \"proident\",\n" +
+                         "      \"incididunt\",\n" +
+                         "      \"qui\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Nelson Wolf\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Corina Fry\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Carlene Bean\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Nicholson Dodson! You have 8 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"strawberry\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f15335f03974b701b22feb\",\n" +
+                         "    \"index\": 2,\n" +
+                         "    \"guid\": \"bf249929-ce37-4702-9671-1935278a2ff8\",\n" +
+                         "    \"isActive\": false,\n" +
+                         "    \"balance\": \"$2,455.81\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 38,\n" +
+                         "    \"eyeColor\": \"blue\",\n" +
+                         "    \"name\": \"Strickland Hodges\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"MACRONAUT\",\n" +
+                         "    \"email\": \"stricklandhodges@macronaut.com\",\n" +
+                         "    \"phone\": \"+1 (806) 573-3642\",\n" +
+                         "    \"address\": \"929 Sutton Street, Caroleen, Delaware, 2273\",\n" +
+                         "    \"about\": \"culpa eiusmod commodo et officia aute exercitation\",\n" +
+                         "    \"registered\": \"2017-10-26T06:25:14 -03:00\",\n" +
+                         "    \"latitude\": 24.973892,\n" +
+                         "    \"longitude\": 50.218781,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"sit\",\n" +
+                         "      \"exercitation\",\n" +
+                         "      \"Lorem\",\n" +
+                         "      \"qui\",\n" +
+                         "      \"reprehenderit\",\n" +
+                         "      \"incididunt\",\n" +
+                         "      \"cupidatat\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Trevino Howard\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Carol Frye\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Kara Parks\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Strickland Hodges! You have 7 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"apple\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f15335c8e74b04cc2cf3de\",\n" +
+                         "    \"index\": 3,\n" +
+                         "    \"guid\": \"450adca9-503b-41a2-b0a5-3ae7c9d68f81\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$3,895.80\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 30,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Judy Scott\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"ZENSOR\",\n" +
+                         "    \"email\": \"judyscott@zensor.com\",\n" +
+                         "    \"phone\": \"+1 (961) 568-2876\",\n" +
+                         "    \"address\": \"359 Williamsburg Street, Venice, Ohio, 6487\",\n" +
+                         "    \"about\": \"in nulla adipisicing non culpa quis do\",\n" +
+                         "    \"registered\": \"2014-01-08T07:06:47 -04:00\",\n" +
+                         "    \"latitude\": -66.571697,\n" +
+                         "    \"longitude\": 92.502775,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"labore\",\n" +
+                         "      \"minim\",\n" +
+                         "      \"adipisicing\",\n" +
+                         "      \"nostrud\",\n" +
+                         "      \"elit\",\n" +
+                         "      \"deserunt\",\n" +
+                         "      \"cupidatat\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Lorna Hines\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Farrell Ryan\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Georgette Elliott\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Judy Scott! You have 8 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"strawberry\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f1533590ac45e8b03b433e\",\n" +
+                         "    \"index\": 4,\n" +
+                         "    \"guid\": \"88b4e813-d6dc-4902-b4f4-c28fca5d8b32\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$2,997.00\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 38,\n" +
+                         "    \"eyeColor\": \"blue\",\n" +
+                         "    \"name\": \"Briggs Shields\",\n" +
+                         "    \"gender\": \"male\",\n" +
+                         "    \"company\": \"XLEEN\",\n" +
+                         "    \"email\": \"briggsshields@xleen.com\",\n" +
+                         "    \"phone\": \"+1 (987) 435-3420\",\n" +
+                         "    \"address\": \"807 Vine Street, Callaghan, New Mexico, 7939\",\n" +
+                         "    \"about\": \"consectetur cupidatat anim pariatur adipisicing adipisicing irure\",\n" +
+                         "    \"registered\": \"2022-02-28T09:49:04 -03:00\",\n" +
+                         "    \"latitude\": 5.401627,\n" +
+                         "    \"longitude\": 64.076763,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"irure\",\n" +
+                         "      \"sint\",\n" +
+                         "      \"aliqua\",\n" +
+                         "      \"officia\",\n" +
+                         "      \"consectetur\",\n" +
+                         "      \"qui\",\n" +
+                         "      \"eiusmod\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Roberts Weeks\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Justice Bullock\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Simone Jacobson\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Briggs Shields! You have 8 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"strawberry\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f153359ee33fe36670766f\",\n" +
+                         "    \"index\": 5,\n" +
+                         "    \"guid\": \"f87bfc03-46d5-4e8f-9f55-dafa25e124cb\",\n" +
+                         "    \"isActive\": false,\n" +
+                         "    \"balance\": \"$2,933.39\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 27,\n" +
+                         "    \"eyeColor\": \"green\",\n" +
+                         "    \"name\": \"Kendra Peterson\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"FUELWORKS\",\n" +
+                         "    \"email\": \"kendrapeterson@fuelworks.com\",\n" +
+                         "    \"phone\": \"+1 (951) 518-3222\",\n" +
+                         "    \"address\": \"155 Dewey Place, Westwood, Georgia, 1647\",\n" +
+                         "    \"about\": \"occaecat in ipsum non cillum proident officia\",\n" +
+                         "    \"registered\": \"2017-04-26T06:50:40 -03:00\",\n" +
+                         "    \"latitude\": -14.371127,\n" +
+                         "    \"longitude\": -0.400474,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"ad\",\n" +
+                         "      \"ipsum\",\n" +
+                         "      \"eiusmod\",\n" +
+                         "      \"cillum\",\n" +
+                         "      \"et\",\n" +
+                         "      \"et\",\n" +
+                         "      \"ipsum\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Orr Stone\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Mavis Mccullough\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Lea Whitfield\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Kendra Peterson! You have 6 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f15335cb2b3032b85383bb\",\n" +
+                         "    \"index\": 6,\n" +
+                         "    \"guid\": \"f1df8765-ebcb-40b9-957e-a04d0b16f2c6\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$2,219.05\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 38,\n" +
+                         "    \"eyeColor\": \"brown\",\n" +
+                         "    \"name\": \"Carissa Hogan\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"SOPRANO\",\n" +
+                         "    \"email\": \"carissahogan@soprano.com\",\n" +
+                         "    \"phone\": \"+1 (919) 432-2299\",\n" +
+                         "    \"address\": \"555 Myrtle Avenue, Yonah, Arkansas, 4416\",\n" +
+                         "    \"about\": \"commodo exercitation ex adipisicing reprehenderit amet ut\",\n" +
+                         "    \"registered\": \"2019-04-02T12:22:13 -03:00\",\n" +
+                         "    \"latitude\": 26.396021,\n" +
+                         "    \"longitude\": -51.909653,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"deserunt\",\n" +
+                         "      \"veniam\",\n" +
+                         "      \"ut\",\n" +
+                         "      \"velit\",\n" +
+                         "      \"elit\",\n" +
+                         "      \"proident\",\n" +
+                         "      \"reprehenderit\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Mcknight Walsh\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Dalton Mclean\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Crystal Poole\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Carissa Hogan! You have 7 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  },\n" +
+                         "  {\n" +
+                         "    \"_id\": \"63f15335754d0e8d9275a344\",\n" +
+                         "    \"index\": 7,\n" +
+                         "    \"guid\": \"4f7f213b-29b1-48e6-b586-c76b609340f0\",\n" +
+                         "    \"isActive\": true,\n" +
+                         "    \"balance\": \"$1,114.53\",\n" +
+                         "    \"picture\": \"http://placehold.it/32x32\",\n" +
+                         "    \"age\": 34,\n" +
+                         "    \"eyeColor\": \"green\",\n" +
+                         "    \"name\": \"Vonda Whitley\",\n" +
+                         "    \"gender\": \"female\",\n" +
+                         "    \"company\": \"APEXTRI\",\n" +
+                         "    \"email\": \"vondawhitley@apextri.com\",\n" +
+                         "    \"phone\": \"+1 (852) 464-2850\",\n" +
+                         "    \"address\": \"115 Rogers Avenue, Mahtowa, Northern Mariana Islands, 8529\",\n" +
+                         "    \"about\": \"cupidatat consequat excepteur consequat incididunt officia esse\",\n" +
+                         "    \"registered\": \"2014-08-15T07:32:32 -04:00\",\n" +
+                         "    \"latitude\": -11.275146,\n" +
+                         "    \"longitude\": 114.522759,\n" +
+                         "    \"tags\": [\n" +
+                         "      \"occaecat\",\n" +
+                         "      \"occaecat\",\n" +
+                         "      \"incididunt\",\n" +
+                         "      \"ea\",\n" +
+                         "      \"et\",\n" +
+                         "      \"id\",\n" +
+                         "      \"eiusmod\"\n" +
+                         "    ],\n" +
+                         "    \"friends\": [\n" +
+                         "      {\n" +
+                         "        \"id\": 0,\n" +
+                         "        \"name\": \"Tania Cunningham\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 1,\n" +
+                         "        \"name\": \"Boone Best\"\n" +
+                         "      },\n" +
+                         "      {\n" +
+                         "        \"id\": 2,\n" +
+                         "        \"name\": \"Nanette Yates\"\n" +
+                         "      }\n" +
+                         "    ],\n" +
+                         "    \"greeting\": \"Hello, Vonda Whitley! You have 1 unread messages.\",\n" +
+                         "    \"favoriteFruit\": \"banana\"\n" +
+                         "  }\n" +
+                         "]");
+    }
+
+    @Test
+    public void testJsonEmptyString() {
+        assertThrows(MalformedJsonException.class, () -> testJson(""));
+    }
+
+    @Test
+    public void testJsonBlankString() {
+        assertThrows(MalformedJsonException.class, () -> testJson("  "));
+    }
+
+    private void testJson(String jsonString) {
+        testJsonVarchar(jsonString);
+        if (unitDbAdapter.supportsJsonType()) {
+            testJsonOther(jsonString);
+        }
+    }
+
+    private void testJsonOther(String jsonString) {
+        JsonOther jsonInsert = context.newObject(JsonOther.class);
+        jsonInsert.setData(new Json(jsonString));
+        context.commitChanges();
+
+        JsonOther jsonSelect = context.selectOne(SelectById.query(JsonOther.class, jsonInsert.getObjectId()));
+        assertEquals(jsonInsert.getData(), jsonSelect.getData());
+    }
+
+    private void testJsonVarchar(String jsonString) {
+        JsonVarchar jsonInsert = context.newObject(JsonVarchar.class);
+        jsonInsert.setData(new Json(jsonString));
+        context.commitChanges();
+
+        JsonVarchar jsonSelect = context.selectOne(SelectById.query(JsonVarchar.class, jsonInsert.getObjectId()));
+        assertEquals(jsonInsert.getData(), jsonSelect.getData());
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonUtilsTest.java b/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonUtilsTest.java
index d60c2a4..d790d58 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonUtilsTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/value/json/JsonUtilsTest.java
@@ -20,55 +20,215 @@
 package org.apache.cayenne.value.json;
 
 import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
 
 /**
  * @since 4.2
  */
+@RunWith(Enclosed.class)
 public class JsonUtilsTest {
 
-    @Test
-    public void compare() {
-        assertTrue(JsonUtils.compare("[]", "[]"));
-        assertTrue(JsonUtils.compare("{}", "{}"));
-        assertFalse(JsonUtils.compare("[]", "{}"));
+    @RunWith(Parameterized.class)
+    public static class CompareTest {
 
-        assertTrue(JsonUtils.compare("123", "123"));
-        assertFalse(JsonUtils.compare("123", "124"));
+        @Parameterized.Parameters(name = " {0} eq {1} ")
+        public static Object[][] data() {
+            return new Object[][]{
+                    {"[]", "[]", true},
+                    {"{}", "{}", true},
+                    {"[]", "{}", false},
 
-        assertTrue(JsonUtils.compare("null", "null"));
-        assertTrue(JsonUtils.compare("true", "true"));
-        assertFalse(JsonUtils.compare("true", "false"));
+                    {"123", "123", true},
+                    {"123", "124", false},
 
-        assertTrue(JsonUtils.compare("\"123\"", "\"123\""));
-        assertFalse(JsonUtils.compare("123", "\"123\""));
+                    {"null", "null", true},
+                    {"true", "true", true},
+                    {"true", "false", false},
 
-        assertTrue(JsonUtils.compare("[1,2,3]", "[1, 2, 3]"));
-        assertFalse(JsonUtils.compare("[1,2,3]", "[1,2,3,4]"));
-        assertFalse(JsonUtils.compare("[1,2,3]", "[1,2]"));
-        assertFalse(JsonUtils.compare("[1,2,3]", "[1,2,4]"));
+                    {"\"123\"", "\"123\"", true},
+                    {"123", "\"123\"", false},
 
-        assertTrue(JsonUtils.compare("{\"abc\":123,\"def\":321}", " {\"def\" :  321 , \n\t\"abc\" :\t123 }"));
-        assertFalse(JsonUtils.compare("{\"abc\":123}", " {\"abc\" :  124 }"));
+                    {"[1,2,3]", "[1, 2, 3]", true},
+                    {"[1,2,3]", "[1,2,3,4]", false},
+                    {"[1,2,3]", "[1,2]", false},
+                    {"[1,2,3]", "[1,2,4]", false},
+
+                    {"{\"abc\":123,\"def\":321}", " {\"def\" :  321 , \n\t\"abc\" :\t123 }", true},
+                    {"{\"abc\":123}", " {\"abc\" :  124 }", false}
+            };
+        }
+
+        @Parameterized.Parameter
+        public String jsonStringA;
+        @Parameterized.Parameter(1)
+        public String jsonStringB;
+        @Parameterized.Parameter(2)
+        public boolean areEquals;
+
+        @Test
+        public void compare() {
+            assertEquals(areEquals, JsonUtils.compare(jsonStringA, jsonStringB));
+        }
     }
 
-    @Test
-    public void normalize() {
-        assertEquals("[]", JsonUtils.normalize("[]"));
-        assertEquals("{}", JsonUtils.normalize("{}"));
-        assertEquals("true", JsonUtils.normalize("true"));
-        assertEquals("null", JsonUtils.normalize("null"));
-        assertEquals("false", JsonUtils.normalize("false"));
-        assertEquals("123", JsonUtils.normalize("123"));
-        assertEquals("-10.24e3", JsonUtils.normalize("-10.24e3"));
-        assertEquals("\"abc\\\"def\"", JsonUtils.normalize("\"abc\\\"def\""));
+    @RunWith(Parameterized.class)
+    public static class NormalizeTest {
 
-        assertEquals("[1, 2.0, -0.3e3, false, null, true]",
-                JsonUtils.normalize("[1 ,  2.0  ,-0.3e3, false,\nnull,\ttrue]"));
-        assertEquals("{\"abc\": 321, \"def\": true, \"ghi\": \"jkl\"}",
-                JsonUtils.normalize("{\"abc\":321,\n\"def\":true,\n\t\"ghi\":\"jkl\"}"));
+        @Parameterized.Parameters(name = " {0} ")
+        public static Object[][] data() {
+            return new Object[][]{
+                    {"[]", "[]", null},
+                    {"{}", "{}", null},
+                    {"true", "true", null},
+                    {"null", "null", null},
+                    {"false", "false", null},
+                    {"123", "123", null},
+                    {"-10.24e3", "-10.24e3", null},
+                    {"\"abc\\\"def\"", "\"abc\\\"def\"", null},
+
+                    {
+                            "[1, 2.0, -0.3e3, false, null, true]",
+                            "[1 ,  2.0  ,-0.3e3, false,\nnull,\ttrue]",
+                            null
+                    },
+                    {
+                            "{\"abc\": 321, \"def\": true, \"ghi\": \"jkl\"}",
+                            "{\"abc\":321,\n\"def\":true,\n\t\"ghi\":\"jkl\"}",
+                            null
+                    },
+                    {
+                            "{\"tags\": [\"ad\", \"irure\", \"anim\"], \"age\": 20}",
+                            "{\"tags\": [\"ad\",\n\"irure\", \"anim\"],\n\"age\": 20}",
+                            null
+                    },
+                    {
+                            "{\"objects\": [{\"id\": 1}, {\"id\": 2}]}",
+                            "{\"objects\":\n[\n{\n\"id\": 1\n},\n{\n\"id\": 2\n}\n]}",
+                            null
+                    },
+                    {
+                            "["
+                            + "{"
+                            + "\"_id\": \"63f218c8ae709e45c7b32c5f\", "
+                            + "\"index\": 0, "
+                            + "\"guid\": \"b3c2b147-9031-40ee-b2a9-fabbd7f5da81\", "
+                            + "\"isActive\": false, "
+                            + "\"balance\": \"$2,836.15\", "
+                            + "\"picture\": \"http://placehold.it/32x32\", "
+                            + "\"age\": 21, "
+                            + "\"eyeColor\": \"green\", "
+                            + "\"name\": \"Ratliff Martin\", "
+                            + "\"gender\": \"male\", "
+                            + "\"company\": \"PLASMOSIS\", "
+                            + "\"email\": \"ratliffmartin@plasmosis.com\", "
+                            + "\"phone\": \"+1 (897) 415-2945\", "
+                            + "\"address\": \"241 Foster Avenue, Outlook, New Jersey, 1479\", "
+                            + "\"about\": \"pariatur irure qui consequat excepteur laborum nulla\", "
+                            + "\"registered\": \"2018-05-18T08:04:15 -03:00\", "
+                            + "\"latitude\": -51.195497, "
+                            + "\"longitude\": 163.317807, "
+                            + "\"tags\": ["
+                            + "\"exercitation\", "
+                            + "\"nulla\", "
+                            + "\"labore\", "
+                            + "\"enim\", "
+                            + "\"ad\", "
+                            + "\"anim\", "
+                            + "\"excepteur\""
+                            + "], "
+                            + "\"friends\": ["
+                            + "{"
+                            + "\"id\": 0, "
+                            + "\"name\": \"Rowena Benson\""
+                            + "}, "
+                            + "{"
+                            + "\"id\": 1, "
+                            + "\"name\": \"Bird Mclaughlin\""
+                            + "}, "
+                            + "{"
+                            + "\"id\": 2, "
+                            + "\"name\": \"Mabel James\""
+                            + "}"
+                            + "], "
+                            + "\"greeting\": \"Hello, Ratliff Martin! You have 2 unread messages.\", "
+                            + "\"favoriteFruit\": \"strawberry\""
+                            + "}"
+                            + "]",
+
+                            "[\n"
+                            + "  {\n"
+                            + "    \"_id\": \"63f218c8ae709e45c7b32c5f\",\n"
+                            + "    \"index\": 0,\n"
+                            + "    \"guid\": \"b3c2b147-9031-40ee-b2a9-fabbd7f5da81\",\n"
+                            + "    \"isActive\": false,\n"
+                            + "    \"balance\": \"$2,836.15\",\n"
+                            + "    \"picture\": \"http://placehold.it/32x32\",\n"
+                            + "    \"age\": 21,\n"
+                            + "    \"eyeColor\": \"green\",\n"
+                            + "    \"name\": \"Ratliff Martin\",\n"
+                            + "    \"gender\": \"male\",\n"
+                            + "    \"company\": \"PLASMOSIS\",\n"
+                            + "    \"email\": \"ratliffmartin@plasmosis.com\",\n"
+                            + "    \"phone\": \"+1 (897) 415-2945\",\n"
+                            + "    \"address\": \"241 Foster Avenue, Outlook, New Jersey, 1479\",\n"
+                            + "    \"about\": \"pariatur irure qui consequat excepteur laborum nulla\",\n"
+                            + "    \"registered\": \"2018-05-18T08:04:15 -03:00\",\n"
+                            + "    \"latitude\": -51.195497,\n"
+                            + "    \"longitude\": 163.317807,\n"
+                            + "    \"tags\": [\n"
+                            + "      \"exercitation\",\n"
+                            + "      \"nulla\",\n"
+                            + "      \"labore\",\n"
+                            + "      \"enim\",\n"
+                            + "      \"ad\",\n"
+                            + "      \"anim\",\n"
+                            + "      \"excepteur\"\n"
+                            + "    ],\n"
+                            + "    \"friends\": [\n"
+                            + "      {\n"
+                            + "        \"id\": 0,\n"
+                            + "        \"name\": \"Rowena Benson\"\n"
+                            + "      },\n"
+                            + "      {\n"
+                            + "        \"id\": 1,\n"
+                            + "        \"name\": \"Bird Mclaughlin\"\n"
+                            + "      },\n"
+                            + "      {\n"
+                            + "        \"id\": 2,\n"
+                            + "        \"name\": \"Mabel James\"\n"
+                            + "      }\n"
+                            + "    ],\n"
+                            + "    \"greeting\": \"Hello, Ratliff Martin! You have 2 unread messages.\",\n"
+                            + "    \"favoriteFruit\": \"strawberry\"\n"
+                            + "  }\n"
+                            + "]",
+
+                            null
+                    },
+
+                    {"", "", MalformedJsonException.class},
+            };
+        }
+
+        @Parameterized.Parameter
+        public String expected;
+        @Parameterized.Parameter(1)
+        public String jsonString;
+        @Parameterized.Parameter(2)
+        public Class<? extends Throwable> throwable;
+
+        @Test
+        public void normalize() {
+            if (throwable == null) {
+                assertEquals(expected, JsonUtils.normalize(jsonString));
+            } else {
+                assertThrows(throwable, () -> JsonUtils.normalize(jsonString));
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/resources/cayenne-json.xml b/cayenne-server/src/test/resources/cayenne-json.xml
new file mode 100644
index 0000000..a49827c
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-json.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+-->
+<domain xmlns="http://cayenne.apache.org/schema/11/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/11/domain https://cayenne.apache.org/schema/11/domain.xsd"
+	 project-version="11">
+	<map name="json"/>
+</domain>
diff --git a/cayenne-server/src/test/resources/json.map.xml b/cayenne-server/src/test/resources/json.map.xml
new file mode 100644
index 0000000..cf237e0
--- /dev/null
+++ b/cayenne-server/src/test/resources/json.map.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+-->
+<data-map xmlns="http://cayenne.apache.org/schema/11/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/11/modelMap https://cayenne.apache.org/schema/11/modelMap.xsd"
+	 project-version="11">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.json"/>
+	<property name="defaultSuperclass" value="org.apache.cayenne.CayenneDataObject"/>
+	<db-entity name="JSON_OTHER">
+		<db-attribute name="DATA" type="OTHER" isMandatory="true"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="JSON_VARCHAR">
+		<db-attribute name="DATA" type="LONGNVARCHAR" isMandatory="true"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<obj-entity name="JsonOther" className="org.apache.cayenne.testdo.json.JsonOther" dbEntityName="JSON_OTHER">
+		<obj-attribute name="data" type="org.apache.cayenne.value.Json" db-attribute-path="DATA"/>
+	</obj-entity>
+	<obj-entity name="JsonVarchar" className="org.apache.cayenne.testdo.json.JsonVarchar" dbEntityName="JSON_VARCHAR">
+		<obj-attribute name="data" type="org.apache.cayenne.value.Json" db-attribute-path="DATA"/>
+	</obj-entity>
+</data-map>
diff --git a/cayenne-server/src/test/resources/relationships-flattened.map.xml b/cayenne-server/src/test/resources/relationships-flattened.map.xml
index a9163c2..e1f3938 100644
--- a/cayenne-server/src/test/resources/relationships-flattened.map.xml
+++ b/cayenne-server/src/test/resources/relationships-flattened.map.xml
@@ -19,11 +19,11 @@
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
 	<db-entity name="ENTITY2">
-		<db-attribute name="ENTITY1_ID" type="INTEGER"/>
+		<db-attribute name="ENTITY1_ID" type="INTEGER" isMandatory="true"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
 	<db-entity name="ENTITY3">
-		<db-attribute name="ENTITY2_ID" type="INTEGER"/>
+		<db-attribute name="ENTITY2_ID" type="INTEGER" isMandatory="true"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
 	<db-entity name="FLATTENED_CIRCULAR">
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
index 16ea152..2bd56fa 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
@@ -22,6 +22,7 @@
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.configuration.DataChannelDescriptor;
 import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.di.DIRuntimeException;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EmbeddedAttribute;
@@ -83,8 +84,8 @@
     }
 
     private static List<ObjAttributeWrapper> wrapObjAttributes(Collection<ObjAttribute> attributes) {
-        List<ObjAttributeWrapper>  wrappedAttributes = new ArrayList<>();
-        for(ObjAttribute attr : attributes) {
+        List<ObjAttributeWrapper> wrappedAttributes = new ArrayList<>();
+        for (ObjAttribute attr : attributes) {
             wrappedAttributes.add(new ObjAttributeWrapper(attr));
         }
         return wrappedAttributes;
@@ -122,7 +123,9 @@
                 : null;
     }
 
-    /** Refreshes DbEntity to current db entity within ObjEntity. */
+    /**
+     * Refreshes DbEntity to current db entity within ObjEntity.
+     */
     public void resetDbEntity() {
         if (dbEntity == entity.getDbEntity()) {
             return;
@@ -196,12 +199,10 @@
         if (dbAttribute == null) {
             if (!attribute.isInherited() && attribute.getEntity().isAbstract()) {
                 return attribute.getDbAttributePath();
-            }
-            else {
+            } else {
                 return null;
             }
-        }
-        else if (attribute.getDbAttributePath() != null && attribute.getDbAttributePath().contains(".")) {
+        } else if (attribute.getDbAttributePath() != null && attribute.getDbAttributePath().contains(".")) {
             return attribute.getDbAttributePath();
         }
         return dbAttribute.getName();
@@ -213,20 +214,15 @@
             if (!(attribute.getValue() instanceof EmbeddedAttribute)) {
                 try {
                     type = TypesMapping.getSqlTypeByJava(attribute.getJavaClass());
-                    // have to catch the exception here to make sure that
-                    // exceptional situations
-                    // (class doesn't exist, for example) don't prevent the gui
-                    // from properly updating.
-                }
-                catch (CayenneRuntimeException cre) {
+                    // have to catch the exception here to make sure that exceptional situations
+                    // (class doesn't exist, for example) don't prevent the gui from properly updating.
+                } catch (CayenneRuntimeException | DIRuntimeException cre) {
                     return null;
                 }
-            }
-            else {
+            } else {
                 return null;
             }
-        }
-        else {
+        } else {
             type = dbAttribute.getType();
         }
         return TypesMapping.getSqlNameByType(type);
@@ -251,7 +247,7 @@
      */
     @Override
     public void resetModel() {
-        for(ObjAttributeWrapper attribute : objectList) {
+        for (ObjAttributeWrapper attribute : objectList) {
             attribute.resetEdits();
         }
     }    
@@ -261,7 +257,7 @@
      */
     @Override
     public boolean isValid() {
-        for(ObjAttributeWrapper attribute : getObjectList()) {
+        for (ObjAttributeWrapper attribute : getObjectList()) {
             if (!attribute.isValid()) {
                 return false;
             }
@@ -348,7 +344,7 @@
         // If db attribute exist, associate it with obj attribute
         if (value != null) {
 
-            if (ProjectUtil.isDbAttributePathCorrect(dbEntity,value.toString())) {
+            if (ProjectUtil.isDbAttributePathCorrect(dbEntity, value.toString())) {
                 attribute.setDbAttributePath(value.toString());
             } else {
                 attribute.setDbAttributePath(null);
@@ -452,7 +448,7 @@
         }
     }
 
-    private class ObjAttributeTableComparator implements Comparator<ObjAttributeWrapper>{
+    private class ObjAttributeTableComparator implements Comparator<ObjAttributeWrapper> {
 
         private final int sortCol;
 
diff --git a/modeler/cayenne-wocompat/src/main/java/org/apache/cayenne/wocompat/EOQuery.java b/modeler/cayenne-wocompat/src/main/java/org/apache/cayenne/wocompat/EOQuery.java
index 6aa2295..c08c3da 100644
--- a/modeler/cayenne-wocompat/src/main/java/org/apache/cayenne/wocompat/EOQuery.java
+++ b/modeler/cayenne-wocompat/src/main/java/org/apache/cayenne/wocompat/EOQuery.java
@@ -31,6 +31,8 @@
 import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.PrefetchTreeNode;
 import org.apache.cayenne.query.SortOrder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -248,6 +250,7 @@
 		private static final String OBJ_C = ":"; // Objective-C syntax addition.
 
 		private static Map<String, Integer> selectorToExpressionBridge;
+		private static final Logger logger = LoggerFactory.getLogger(EOFetchSpecificationParser.class);
 
 		/**
 		 * selectorToExpressionBridge is just a mapping of EOModeler's selector
@@ -473,7 +476,7 @@
 				try {
 					keyExp = entity.translateToDbPath(keyExp);
 				} catch (Exception dbpathEx) {
-					return null;
+					logger.warn("Couldn't find " + keyExp + " in " + entity.getName() + " in EOModel");
 				}
 			}
 
@@ -484,6 +487,7 @@
 				exp.setOperand(1, comparisonValue);
 				return exp;
 			} catch (ExpressionException e) {
+				logger.warn(e.getUnlabeledMessage());
 				return null;
 			}
 		}
diff --git a/pom.xml b/pom.xml
index 3ac9188..18c971a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1075,7 +1075,7 @@
 					<dependency>
 						<groupId>com.oracle.database.jdbc</groupId>
 						<artifactId>ojdbc-bom</artifactId>
-						<version>21.6.0.0.1</version>
+						<version>21.8.0.0</version>
 						<type>pom</type>
 						<scope>import</scope>
 					</dependency>
@@ -1084,7 +1084,7 @@
 			<dependencies>
 				<dependency>
 					<groupId>com.oracle.database.jdbc</groupId>
-					<artifactId>ojdbc8</artifactId>
+					<artifactId>ojdbc11</artifactId>
 					<scope>test</scope>
 				</dependency>
 			</dependencies>
@@ -1102,7 +1102,7 @@
 					<dependency>
 						<groupId>com.oracle.database.jdbc</groupId>
 						<artifactId>ojdbc-bom</artifactId>
-						<version>21.6.0.0.1</version>
+						<version>21.8.0.0</version>
 						<type>pom</type>
 						<scope>import</scope>
 					</dependency>
@@ -1111,7 +1111,7 @@
 			<dependencies>
 				<dependency>
 					<groupId>com.oracle.database.jdbc</groupId>
-					<artifactId>ojdbc8</artifactId>
+					<artifactId>ojdbc11</artifactId>
 					<scope>test</scope>
 				</dependency>
 			</dependencies>