Repository: googleapis/java-spanner-jdbc
Pull request #447 (2021-04-28 09:34:31)

feat: support JSON data type
Adds support for JSON data type.


olavloite: @zoercai @thiagotnunes This is an initial implementation for supporting JSON in JDBC based on the current state of https://github.com/googleapis/java-spanner/pull/872. It assumes the following:
1. JSON will be treated as a String whenever possible. So for example to get a JSON value from a `ResultSet`, you would normally call `ResultSet#getString(int)` (or alternatively `ResultSet#getObject(int)` which will also return the JSON value as a String).
2. When setting a JSON value as a parameter on a `PreparedStatement`, we need to make a distinction between String and JSON. That is because the backend does not support automatic coercion from String to JSON, which means that the JDBC driver somehow needs to know that the String value that was set should in some way be treated as JSON. That can be achieved using one of the following strategies:
2.1. Call `PreparedStatement#setObject(int, Object, int)` where the value is passed as a String and the last parameter is the vendor type code for JSON in Cloud Spanner (`com.google.cloud.spanner.jdbc.JsonType.VENDOR_TYPE_NUMBER`).
2.2. Call `PreparedStatement#setObject(int, Object)` where the value is passed as a `Value.json("some-json-string")`.

The above handling is [similar to how this is also handled](https://stackoverflow.com/questions/35844138/how-can-i-insert-json-object-into-postgres-using-java-preparedstatement) in the JDBC driver of PostgreSQL.
zoercai: Hi @olavloite , I've merged the JSON Java PR and started the release https://github.com/googleapis/java-spanner/pull/1364. Please rebase and rerun CI when the release is done, and merge this PR once CI passes. Thanks!
zoercai: update: the release is done.
generated-files-bot[bot]: <!-- probot comment [12634855]-->
*Warning*: This pull request is touching the following templated files:

* .github/generated-files-bot.yml
* .github/workflows/ci.yaml
* .kokoro/build.sh
* .kokoro/coerce_logs.sh
* .kokoro/dependencies.sh
* .kokoro/nightly/integration.cfg
* .kokoro/nightly/samples.cfg
* .kokoro/release/publish_javadoc11.sh
* SECURITY.md
* java.header
* renovate.json
* samples/install-without-bom/pom.xml
* samples/pom.xml
* samples/snapshot/pom.xml
* samples/snippets/pom.xml

Patch:
diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml
index 26d3273..8a02400 100644
--- a/.github/sync-repo-settings.yaml
+++ b/.github/sync-repo-settings.yaml
@@ -31,7 +31,6 @@ branchProtectionRules:
   requiredStatusCheckContexts:
     - "dependencies (8)"
     - "dependencies (11)"
-    - "linkage-monitor"
     - "lint"
     - "clirr"
     - "units (8)"
@@ -57,7 +56,6 @@ branchProtectionRules:
   requiredStatusCheckContexts:
     - "dependencies (8)"
     - "dependencies (11)"
-    - "linkage-monitor"
     - "lint"
     - "clirr"
     - "units (7)"
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
index 9f97524..5f29673 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
@@ -48,6 +48,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     if (type.equals(Type.int64())) return Types.BIGINT;
     if (type.equals(Type.numeric())) return Types.NUMERIC;
     if (type.equals(Type.string())) return Types.NVARCHAR;
+    if (type.equals(Type.json())) return Types.NVARCHAR;
     if (type.equals(Type.timestamp())) return Types.TIMESTAMP;
     if (type.getCode() == Code.ARRAY) return Types.ARRAY;
     return Types.OTHER;
@@ -106,6 +107,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
     if (type == Type.int64()) return Long.class.getName();
     if (type == Type.numeric()) return BigDecimal.class.getName();
     if (type == Type.string()) return String.class.getName();
+    if (type == Type.json()) return String.class.getName();
     if (type == Type.timestamp()) return Timestamp.class.getName();
     if (type.getCode() == Code.ARRAY) {
       if (type.getArrayElementType() == Type.bool()) return Boolean[].class.getName();
@@ -115,6 +117,7 @@ abstract class AbstractJdbcWrapper implements Wrapper {
       if (type.getArrayElementType() == Type.int64()) return Long[].class.getName();
       if (type.getArrayElementType() == Type.numeric()) return BigDecimal[].class.getName();
       if (type.getArrayElementType() == Type.string()) return String[].class.getName();
+      if (type.getArrayElementType() == Type.json()) return String[].class.getName();
       if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName();
     }
     return null;
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
index 2a9d9e7..4367740 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
@@ -21,6 +21,7 @@ import com.google.cloud.spanner.ResultSets;
 import com.google.cloud.spanner.Struct;
 import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Type.StructField;
+import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.ValueBinder;
 import com.google.common.collect.ImmutableList;
 import com.google.rpc.Code;
@@ -201,6 +202,9 @@ class JdbcArray implements Array {
           case STRING:
             builder = binder.to((String) value);
             break;
+          case JSON:
+            builder = binder.to(Value.json((String) value));
+            break;
           case TIMESTAMP:
             builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
             break;
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index 6e6c3bf..8da36e4 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
@@ -229,6 +229,32 @@ enum JdbcDataType {
       return Type.string();
     }
   },
+  JSON {
+    @Override
+    public int getSqlType() {
+      return JsonType.VENDOR_TYPE_NUMBER;
+    }
+
+    @Override
+    public Class<String> getJavaClass() {
+      return String.class;
+    }
+
+    @Override
+    public Code getCode() {
+      return Code.JSON;
+    }
+
+    @Override
+    public List<String> getArrayElements(ResultSet rs, int columnIndex) {
+      return rs.getJsonList(columnIndex);
+    }
+
+    @Override
+    public Type getSpannerType() {
+      return Type.json();
+    }
+  },
   TIMESTAMP {
     @Override
     public int getSqlType() {
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
index 75f16cd..24773cd 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
@@ -19,6 +19,7 @@ package com.google.cloud.spanner.jdbc;
 import com.google.cloud.ByteArray;
 import com.google.cloud.spanner.Statement;
 import com.google.cloud.spanner.Statement.Builder;
+import com.google.cloud.spanner.Type;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.ValueBinder;
 import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
@@ -264,6 +265,7 @@ class JdbcParameterStore {
       case Types.NCLOB:
       case Types.NUMERIC:
       case Types.DECIMAL:
+      case JsonType.VENDOR_TYPE_NUMBER:
         return true;
     }
     return false;
@@ -315,6 +317,11 @@ class JdbcParameterStore {
         return value instanceof Clob || value instanceof Reader;
       case Types.NCLOB:
         return value instanceof NClob || value instanceof Reader;
+      case JsonType.VENDOR_TYPE_NUMBER:
+        return value instanceof String
+            || value instanceof InputStream
+            || value instanceof Reader
+            || (value instanceof Value && ((Value) value).getType().getCode() == Type.Code.JSON);
     }
     return false;
   }
@@ -544,30 +551,34 @@ class JdbcParameterStore {
       case Types.NCHAR:
       case Types.NVARCHAR:
       case Types.LONGNVARCHAR:
+        String stringValue;
         if (value instanceof String) {
-          return binder.to((String) value);
+          stringValue = (String) value;
         } else if (value instanceof InputStream) {
-          InputStreamReader reader =
-              new InputStreamReader((InputStream) value, StandardCharsets.US_ASCII);
-          try {
-            return binder.to(CharStreams.toString(reader));
-          } catch (IOException e) {
-            throw JdbcSqlExceptionFactory.of(
-                "could not set string from input stream", Code.INVALID_ARGUMENT, e);
-          }
+          stringValue = getStringFromInputStream((InputStream) value);
         } else if (value instanceof Reader) {
-          try {
-            return binder.to(CharStreams.toString((Reader) value));
-          } catch (IOException e) {
-            throw JdbcSqlExceptionFactory.of(
-                "could not set string from reader", Code.INVALID_ARGUMENT, e);
-          }
+          stringValue = getStringFromReader((Reader) value);
         } else if (value instanceof URL) {
-          return binder.to(((URL) value).toString());
+          stringValue = ((URL) value).toString();
         } else if (value instanceof UUID) {
-          return binder.to(((UUID) value).toString());
+          stringValue = ((UUID) value).toString();
+        } else {
+          throw JdbcSqlExceptionFactory.of(value + " is not a valid string", Code.INVALID_ARGUMENT);
+        }
+        return binder.to(stringValue);
+      case JsonType.VENDOR_TYPE_NUMBER:
+        String jsonValue;
+        if (value instanceof String) {
+          jsonValue = (String) value;
+        } else if (value instanceof InputStream) {
+          jsonValue = getStringFromInputStream((InputStream) value);
+        } else if (value instanceof Reader) {
+          jsonValue = getStringFromReader((Reader) value);
+        } else {
+          throw JdbcSqlExceptionFactory.of(
+              value + " is not a valid JSON value", Code.INVALID_ARGUMENT);
         }
-        throw JdbcSqlExceptionFactory.of(value + " is not a valid string", Code.INVALID_ARGUMENT);
+        return binder.to(Value.json(jsonValue));
       case Types.DATE:
         if (value instanceof Date) {
           return binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
@@ -652,6 +663,25 @@ class JdbcParameterStore {
     return null;
   }
 
+  private String getStringFromInputStream(InputStream inputStream) throws SQLException {
+    InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII);
+    try {
+      return CharStreams.toString(reader);
+    } catch (IOException e) {
+      throw JdbcSqlExceptionFactory.of(
+          "could not set string from input stream", Code.INVALID_ARGUMENT, e);
+    }
+  }
+
+  private String getStringFromReader(Reader reader) throws SQLException {
+    try {
+      return CharStreams.toString(reader);
+    } catch (IOException e) {
+      throw JdbcSqlExceptionFactory.of(
+          "could not set string from reader", Code.INVALID_ARGUMENT, e);
+    }
+  }
+
   /** Set the parameter value based purely on the type of the value. */
   private Builder setParamWithUnknownType(ValueBinder<Builder> binder, Object value)
       throws SQLException {
@@ -769,14 +799,16 @@ class JdbcParameterStore {
         case Types.LONGNVARCHAR:
         case Types.CLOB:
         case Types.NCLOB:
-          return binder.toStringArray((Iterable<String>) null);
+          return binder.toStringArray(null);
+        case JsonType.VENDOR_TYPE_NUMBER:
+          return binder.toJsonArray(null);
         case Types.DATE:
-          return binder.toDateArray((Iterable<com.google.cloud.Date>) null);
+          return binder.toDateArray(null);
         case Types.TIME:
         case Types.TIME_WITH_TIMEZONE:
         case Types.TIMESTAMP:
         case Types.TIMESTAMP_WITH_TIMEZONE:
-          return binder.toTimestampArray((Iterable<com.google.cloud.Timestamp>) null);
+          return binder.toTimestampArray(null);
         case Types.BINARY:
         case Types.VARBINARY:
         case Types.LONGVARBINARY:
@@ -829,7 +861,11 @@ class JdbcParameterStore {
     } else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
       return binder.toTimestampArray(JdbcTypeConverter.toGoogleTimestamps((Timestamp[]) value));
     } else if (String[].class.isAssignableFrom(value.getClass())) {
-      return binder.toStringArray(Arrays.asList((String[]) value));
+      if (type == JsonType.VENDOR_TYPE_NUMBER) {
+        return binder.toJsonArray(Arrays.asList((String[]) value));
+      } else {
+        return binder.toStringArray(Arrays.asList((String[]) value));
+      }
     } else if (byte[][].class.isAssignableFrom(value.getClass())) {
       return binder.toBytesArray(JdbcTypeConverter.toGoogleBytes((byte[][]) value));
     }
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
index c4ca6b5..489c2d2 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
@@ -142,6 +142,8 @@ class JdbcResultSet extends AbstractJdbcResultSet {
         return isNull ? null : spanner.getBigDecimal(spannerIndex).toString();
       case STRING:
         return isNull ? null : spanner.getString(spannerIndex);
+      case JSON:
+        return isNull ? null : spanner.getJson(spannerIndex);
       case TIMESTAMP:
         return isNull ? null : spanner.getTimestamp(spannerIndex).toString();
       case STRUCT:
@@ -169,6 +171,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? false : Boolean.valueOf(spanner.getString(spannerIndex));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -198,6 +201,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -227,6 +231,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -256,6 +261,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex)));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -283,6 +289,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? 0L : parseLong(spanner.getString(spannerIndex));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -310,6 +317,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex)));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -337,6 +345,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case STRING:
         return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
       case BYTES:
+      case JSON:
       case DATE:
       case STRUCT:
       case TIMESTAMP:
@@ -372,6 +381,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
@@ -396,6 +406,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
@@ -421,6 +432,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
@@ -576,6 +588,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
     if (type == Type.int64()) return getLong(columnIndex);
     if (type == Type.numeric()) return getBigDecimal(columnIndex);
     if (type == Type.string()) return getString(columnIndex);
+    if (type == Type.json()) return getString(columnIndex);
     if (type == Type.timestamp()) return getTimestamp(columnIndex);
     if (type.getCode() == Code.ARRAY) return getArray(columnIndex);
     throw JdbcSqlExceptionFactory.of(
@@ -664,6 +677,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
               e);
         }
       case BYTES:
+      case JSON:
       case DATE:
       case TIMESTAMP:
       case STRUCT:
@@ -749,6 +763,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
@@ -778,6 +793,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
@@ -810,6 +826,7 @@ class JdbcResultSet extends AbstractJdbcResultSet {
       case INT64:
       case NUMERIC:
       case BYTES:
+      case JSON:
       case STRUCT:
       case ARRAY:
       default:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
index 6aa9b48..4b2e4f0 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
@@ -81,7 +81,8 @@ class JdbcTypeConverter {
       }
       if (targetType.equals(byte[].class)) {
         if (type.getCode() == Code.BYTES) return value;
-        if (type.getCode() == Code.STRING) return ((String) value).getBytes(UTF8);
+        if (type.getCode() == Code.STRING || type.getCode() == Code.JSON)
+          return ((String) value).getBytes(UTF8);
       }
       if (targetType.equals(Boolean.class)) {
         if (type.getCode() == Code.BOOL) return value;
@@ -186,6 +187,8 @@ class JdbcTypeConverter {
           case TIMESTAMP:
             return Value.timestampArray(
                 toGoogleTimestamps((java.sql.Timestamp[]) ((java.sql.Array) value).getArray()));
+          case JSON:
+            return Value.jsonArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray()));
           case STRUCT:
           default:
             throw JdbcSqlExceptionFactory.of(
@@ -207,6 +210,8 @@ class JdbcTypeConverter {
         return Value.string((String) value);
       case TIMESTAMP:
         return Value.timestamp(toGoogleTimestamp((java.sql.Timestamp) value));
+      case JSON:
+        return Value.json((String) value);
       case STRUCT:
       default:
         throw JdbcSqlExceptionFactory.of(
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java b/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java
new file mode 100644
index 0000000..44af26b
--- /dev/null
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed 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
+ *
+ *       http://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 com.google.cloud.spanner.jdbc;
+
+import com.google.spanner.v1.TypeCode;
+import java.sql.PreparedStatement;
+import java.sql.SQLType;
+
+/**
+ * Custom SQL type for Spanner JSON data type. This type (or the vendor type number) must be used
+ * when setting a JSON parameter using {@link PreparedStatement#setObject(int, Object, SQLType)}.
+ */
+public class JsonType implements SQLType {
+  public static final JsonType INSTANCE = new JsonType();
+  /**
+   * Spanner does not have any type numbers, but the code values are unique. Add 100,000 to avoid
+   * conflicts with the type numbers in java.sql.Types.
+   */
+  public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.JSON_VALUE;
+
+  private JsonType() {}
+
+  @Override
+  public String getName() {
+    return "JSON";
+  }
+
+  @Override
+  public String getVendor() {
+    return JsonType.class.getPackage().getName();
+  }
+
+  @Override
+  public Integer getVendorTypeNumber() {
+    return VENDOR_TYPE_NUMBER;
+  }
+
+  public String toString() {
+    return getName();
+  }
+}
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
index 69b1f6a..14106ee 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
@@ -24,6 +24,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
     WHEN SPANNER_TYPE = 'INT64' THEN -5
     WHEN SPANNER_TYPE = 'NUMERIC' THEN 2
     WHEN SPANNER_TYPE LIKE 'STRING%' THEN -9
+    WHEN SPANNER_TYPE = 'JSON' THEN -9
     WHEN SPANNER_TYPE = 'TIMESTAMP' THEN 93
   END AS DATA_TYPE,
   SPANNER_TYPE AS TYPE_NAME,
@@ -36,6 +37,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
         WHEN SPANNER_TYPE = 'BOOL' OR SPANNER_TYPE = 'ARRAY<BOOL>' THEN NULL
         WHEN SPANNER_TYPE = 'DATE' OR SPANNER_TYPE = 'ARRAY<DATE>' THEN 10
         WHEN SPANNER_TYPE = 'TIMESTAMP' OR SPANNER_TYPE = 'ARRAY<TIMESTAMP>' THEN 35
+        WHEN SPANNER_TYPE = 'JSON' OR SPANNER_TYPE = 'ARRAY<JSON>' THEN 2621440
         ELSE 0
       END
     ELSE CAST(REPLACE(SUBSTR(SPANNER_TYPE, STRPOS(SPANNER_TYPE, '(')+1, STRPOS(SPANNER_TYPE, ')')-STRPOS(SPANNER_TYPE, '(')-1), 'MAX', CASE WHEN UPPER(SPANNER_TYPE) LIKE '%STRING%' THEN '2621440' ELSE '10485760' END) AS INT64)
@@ -63,6 +65,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU
   CASE
     WHEN (SPANNER_TYPE LIKE 'STRING%' OR SPANNER_TYPE LIKE 'ARRAY<STRING%') THEN
       cast(replace(substr(spanner_type, strpos(spanner_type, '(')+1, strpos(spanner_type,')')-strpos(spanner_type, '(')-1), 'MAX', '2621440') as INT64)
+    WHEN (SPANNER_TYPE = 'JSON' OR SPANNER_TYPE = 'ARRAY<JSON>') THEN 2621440
     ELSE NULL
   END AS CHAR_OCTET_LENGTH,
   ORDINAL_POSITION,


Test patch:
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
index 6361ce4..67af317 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
@@ -24,6 +24,7 @@ import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier;
 import com.google.cloud.spanner.connection.ConnectionOptions;
 import com.google.cloud.spanner.connection.ITAbstractSpannerTest;
 import com.google.cloud.spanner.jdbc.JdbcSqlScriptVerifier.JdbcGenericConnection;
+import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import java.io.IOException;
@@ -167,9 +168,14 @@ public class ITAbstractJdbcTest {
       try (Connection connection = createConnection()) {
         connection.setAutoCommit(true);
         if (!tableExists(connection, "Singers")) {
+          String scriptFile;
+          if (EmulatorSpannerHelper.isUsingEmulator()) {
+            scriptFile = "CreateMusicTables_Emulator.sql";
+          } else {
+            scriptFile = "CreateMusicTables.sql";
+          }
           for (String statement :
-              AbstractSqlScriptVerifier.readStatementsFromFile(
-                  "CreateMusicTables.sql", getClass())) {
+              AbstractSqlScriptVerifier.readStatementsFromFile(scriptFile, getClass())) {
             connection.createStatement().execute(statement);
           }
         }
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
index f8cacd9..74b8354 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
@@ -19,7 +19,10 @@ package com.google.cloud.spanner.jdbc;
 import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.toSqlDate;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.cloud.spanner.ErrorCode;
@@ -147,6 +150,25 @@ public class JdbcArrayTest {
       assertThat(rs.next()).isFalse();
     }
 
+    array =
+        JdbcArray.createArray(
+            "JSON",
+            new String[] {"{}", "[]", null, "{\"name\":\"John\", \"age\":30, \"car\":null}"});
+    assertThat(array.getBaseType()).isEqualTo(JsonType.VENDOR_TYPE_NUMBER);
+    assertThat(((String[]) array.getArray(1, 1))[0]).isEqualTo("{}");
+    try (ResultSet rs = array.getResultSet()) {
+      assertTrue(rs.next());
+      assertEquals("{}", rs.getString(2));
+      assertTrue(rs.next());
+      assertEquals("[]", rs.getString(2));
+      assertTrue(rs.next());
+      assertNull(rs.getString(2));
+      assertTrue(rs.wasNull());
+      assertTrue(rs.next());
+      assertEquals("{\"name\":\"John\", \"age\":30, \"car\":null}", rs.getString(2));
+      assertFalse(rs.next());
+    }
+
     array =
         JdbcArray.createArray(
             "TIMESTAMP",
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
index 0515188..9d12b1b 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
@@ -181,6 +181,15 @@ public class JdbcParameterStoreTest {
     assertTrue(stringReadersEqual((StringReader) params.getParameter(1), new StringReader("test")));
     verifyParameter(params, Value.string("test"));
 
+    String jsonString = "{\"test\": \"value\"}";
+    params.setParameter(1, jsonString, JsonType.VENDOR_TYPE_NUMBER);
+    assertEquals(jsonString, params.getParameter(1));
+    verifyParameter(params, Value.json(jsonString));
+
+    params.setParameter(1, jsonString, JsonType.INSTANCE);
+    assertEquals(jsonString, params.getParameter(1));
+    verifyParameter(params, Value.json(jsonString));
+
     params.setParameter(1, BigDecimal.ONE, Types.DECIMAL);
     verifyParameter(params, Value.numeric(BigDecimal.ONE));
 
@@ -416,7 +425,8 @@ public class JdbcParameterStoreTest {
           Types.LONGVARCHAR,
           Types.NCHAR,
           Types.NVARCHAR,
-          Types.LONGNVARCHAR
+          Types.LONGNVARCHAR,
+          JsonType.VENDOR_TYPE_NUMBER
         }) {
       assertInvalidParameter(params, new Object(), type);
       assertInvalidParameter(params, Boolean.TRUE, type);
@@ -441,7 +451,8 @@ public class JdbcParameterStoreTest {
           Types.LONGVARCHAR,
           Types.NCHAR,
           Types.NVARCHAR,
-          Types.LONGNVARCHAR
+          Types.LONGNVARCHAR,
+          JsonType.VENDOR_TYPE_NUMBER
         }) {
       Reader reader = new StringReader("test");
       reader.close();
@@ -529,6 +540,11 @@ public class JdbcParameterStoreTest {
     params.setParameter(1, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), (Integer) null);
     assertEquals(UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e"), params.getParameter(1));
     verifyParameter(params, Value.string("83b988cf-1f4e-428a-be3d-cc712621942e"));
+
+    String jsonString = "{\"test\": \"value\"}";
+    params.setParameter(1, Value.json(jsonString), (Integer) null);
+    assertEquals(Value.json(jsonString), params.getParameter(1));
+    verifyParameter(params, Value.json(jsonString));
   }
 
   private boolean stringReadersEqual(StringReader r1, StringReader r2) throws IOException {
@@ -683,6 +699,26 @@ public class JdbcParameterStoreTest {
     params.setParameter(1, JdbcArray.createArray("STRING", null), Types.ARRAY);
     assertEquals(JdbcArray.createArray("STRING", null), params.getParameter(1));
     verifyParameter(params, Value.stringArray(null));
+
+    String jsonString1 = "{\"test\": \"value1\"}";
+    String jsonString2 = "{\"test\": \"value2\"}";
+    params.setParameter(
+        1,
+        JdbcArray.createArray("JSON", new String[] {jsonString1, jsonString2, null}),
+        Types.ARRAY);
+    assertEquals(
+        JdbcArray.createArray("JSON", new String[] {jsonString1, jsonString2, null}),
+        params.getParameter(1));
+    verifyParameter(params, Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null)));
+
+    params.setParameter(1, JdbcArray.createArray("JSON", null), Types.ARRAY);
+    assertEquals(JdbcArray.createArray("JSON", null), params.getParameter(1));
+    verifyParameter(params, Value.jsonArray(null));
+
+    params.setParameter(1, Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null)));
+    assertEquals(
+        Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null)), params.getParameter(1));
+    verifyParameter(params, Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null)));
   }
 
   private void verifyParameter(JdbcParameterStore params, Value value) throws SQLException {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java
index e97382d..b3fcffc 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java
@@ -74,6 +74,7 @@ public class JdbcResultSetMetaDataTest {
       if (type == Type.int64()) return 10;
       if (type == Type.timestamp()) return 24;
       if (type == Type.string()) return 50;
+      if (type == Type.json()) return 50;
       if (type == Type.bytes()) return 50;
       return 50;
     }
@@ -182,6 +183,7 @@ public class JdbcResultSetMetaDataTest {
     types.add(Type.float64());
     types.add(Type.int64());
     types.add(Type.string());
+    types.add(Type.json());
     types.add(Type.timestamp());
     List<Type> arrayTypes = new ArrayList<>();
     for (Type type : types) {
@@ -219,6 +221,7 @@ public class JdbcResultSetMetaDataTest {
     if (type == Type.float64()) return Value.float64(123.45D);
     if (type == Type.int64()) return Value.int64(12345L);
     if (type == Type.string()) return Value.string("test value " + row);
+    if (type == Type.json()) return Value.json("{\"test_value\": " + row + "}");
     if (type == Type.timestamp()) return Value.timestamp(com.google.cloud.Timestamp.now());
 
     if (type.getCode() == Code.ARRAY) {
@@ -240,6 +243,9 @@ public class JdbcResultSetMetaDataTest {
         return Value.int64Array(Arrays.asList(12345L, 54321L));
       if (type.getArrayElementType() == Type.string())
         return Value.stringArray(Arrays.asList("test value " + row, "test value " + row));
+      if (type.getArrayElementType() == Type.json())
+        return Value.jsonArray(
+            Arrays.asList("{\"test_value\": " + row + "}", "{\"test_value\": " + row + "}"));
       if (type.getArrayElementType() == Type.timestamp())
         return Value.timestampArray(
             Arrays.asList(com.google.cloud.Timestamp.now(), com.google.cloud.Timestamp.now()));
@@ -263,7 +269,9 @@ public class JdbcResultSetMetaDataTest {
   public void testIsCaseSensitive() throws SQLException {
     for (int i = 1; i <= TEST_COLUMNS.size(); i++) {
       Type type = TEST_COLUMNS.get(i - 1).type;
-      assertEquals(type == Type.string() || type == Type.bytes(), subject.isCaseSensitive(i));
+      assertEquals(
+          type == Type.string() || type == Type.bytes() || type == Type.json(),
+          subject.isCaseSensitive(i));
     }
   }
 
@@ -319,6 +327,7 @@ public class JdbcResultSetMetaDataTest {
       int length = subject.getPrecision(column);
       return length == 0 ? 50 : length;
     }
+    if (type == Type.json()) return 50;
     if (type == Type.timestamp()) return 16;
     return 10;
   }
@@ -397,6 +406,7 @@ public class JdbcResultSetMetaDataTest {
     if (type == Type.float64()) return Types.DOUBLE;
     if (type == Type.int64()) return Types.BIGINT;
     if (type == Type.string()) return Types.NVARCHAR;
+    if (type == Type.json()) return Types.NVARCHAR;
     if (type == Type.timestamp()) return Types.TIMESTAMP;
     if (type.getCode() == Code.ARRAY) return Types.ARRAY;
     return Types.OTHER;
@@ -446,6 +456,7 @@ public class JdbcResultSetMetaDataTest {
     if (type == Type.float64()) return Double.class.getName();
     if (type == Type.int64()) return Long.class.getName();
     if (type == Type.string()) return String.class.getName();
+    if (type == Type.json()) return String.class.getName();
     if (type == Type.timestamp()) return Timestamp.class.getName();
     if (type.getCode() == Code.ARRAY) {
       if (type.getArrayElementType() == Type.bool()) return Boolean[].class.getName();
@@ -454,6 +465,7 @@ public class JdbcResultSetMetaDataTest {
       if (type.getArrayElementType() == Type.float64()) return Double[].class.getName();
       if (type.getArrayElementType() == Type.int64()) return Long[].class.getName();
       if (type.getArrayElementType() == Type.string()) return String[].class.getName();
+      if (type.getArrayElementType() == Type.json()) return String[].class.getName();
       if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName();
     }
     return null;
@@ -466,15 +478,17 @@ public class JdbcResultSetMetaDataTest {
           + "Col 4: COL4 FLOAT64\n"
           + "Col 5: COL5 INT64\n"
           + "Col 6: COL6 STRING\n"
-          + "Col 7: COL7 TIMESTAMP\n"
-          + "Col 8: COL8 ARRAY\n"
+          + "Col 7: COL7 JSON\n"
+          + "Col 8: COL8 TIMESTAMP\n"
           + "Col 9: COL9 ARRAY\n"
           + "Col 10: COL10 ARRAY\n"
           + "Col 11: COL11 ARRAY\n"
           + "Col 12: COL12 ARRAY\n"
           + "Col 13: COL13 ARRAY\n"
           + "Col 14: COL14 ARRAY\n"
-          + "Col 15: CALCULATED INT64\n";
+          + "Col 15: COL15 ARRAY\n"
+          + "Col 16: COL16 ARRAY\n"
+          + "Col 17: CALCULATED INT64\n";
 
   @Test
   public void testToString() {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
index 9fa5f3e..506992c 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
@@ -132,6 +132,11 @@ public class JdbcResultSetTest {
   private static final BigDecimal NUMERIC_VALUE = new BigDecimal("3.14");
   private static final int NUMERIC_COLINDEX_NULL = 25;
   private static final int NUMERIC_COLINDEX_NOTNULL = 26;
+  private static final String JSON_COL_NULL = "JSON_COL_NULL";
+  private static final String JSON_COL_NOT_NULL = "JSON_COL_NOT_NULL";
+  private static final int JSON_COLINDEX_NULL = 27;
+  private static final int JSON_COLINDEX_NOT_NULL = 28;
+  private static final String JSON_VALUE = "{\"name\":\"John\", \"age\":30, \"car\":null}";
 
   private static final String BOOL_ARRAY_COL = "BOOL_ARRAY";
   private static final List<Boolean> BOOL_ARRAY_VALUE = Arrays.asList(true, null, false);
@@ -149,6 +154,8 @@ public class JdbcResultSetTest {
   private static final List<String> STRING_ARRAY_VALUE = Arrays.asList(STRING_VALUE, null);
   private static final String TIMESTAMP_ARRAY_COL = "TIMESTAMP_ARRAY";
   private static final List<Timestamp> TIMESTAMP_ARRAY_VALUE = Arrays.asList(TIMESTAMP_VALUE, null);
+  private static final String JSON_ARRAY_COL = "JSON_ARRAY";
+  private static final List<String> JSON_ARRAY_VALUE = Arrays.asList(JSON_VALUE, null);
 
   private JdbcResultSet subject;
 
@@ -181,12 +188,15 @@ public class JdbcResultSetTest {
             StructField.of(STRING_COL_TIME, Type.string()),
             StructField.of(NUMERIC_COL_NULL, Type.numeric()),
             StructField.of(NUMERIC_COL_NOT_NULL, Type.numeric()),
+            StructField.of(JSON_COL_NULL, Type.json()),
+            StructField.of(JSON_COL_NOT_NULL, Type.json()),
             StructField.of(BOOL_ARRAY_COL, Type.array(Type.bool())),
             StructField.of(BYTES_ARRAY_COL, Type.array(Type.bytes())),
             StructField.of(DATE_ARRAY_COL, Type.array(Type.date())),
             StructField.of(FLOAT64_ARRAY_COL, Type.array(Type.float64())),
             StructField.of(INT64_ARRAY_COL, Type.array(Type.int64())),
             StructField.of(NUMERIC_ARRAY_COL, Type.array(Type.numeric())),
+            StructField.of(JSON_ARRAY_COL, Type.array(Type.json())),
             StructField.of(STRING_ARRAY_COL, Type.array(Type.string())),
             StructField.of(TIMESTAMP_ARRAY_COL, Type.array(Type.timestamp()))),
         Arrays.asList(
@@ -243,6 +253,10 @@ public class JdbcResultSetTest {
                 .to((BigDecimal) null)
                 .set(NUMERIC_COL_NOT_NULL)
                 .to(NUMERIC_VALUE)
+                .set(JSON_COL_NULL)
+                .to(Value.json(null))
+                .set(JSON_COL_NOT_NULL)
+                .to(Value.json(JSON_VALUE))
                 .set(BOOL_ARRAY_COL)
                 .toBoolArray(BOOL_ARRAY_VALUE)
                 .set(BYTES_ARRAY_COL)
@@ -255,6 +269,8 @@ public class JdbcResultSetTest {
                 .toInt64Array(INT64_ARRAY_VALUE)
                 .set(NUMERIC_ARRAY_COL)
                 .toNumericArray(NUMERIC_ARRAY_VALUE)
+                .set(JSON_ARRAY_COL)
+                .toJsonArray(JSON_ARRAY_VALUE)
                 .set(STRING_ARRAY_COL)
                 .toStringArray(STRING_ARRAY_VALUE)
                 .set(TIMESTAMP_ARRAY_COL)
@@ -322,6 +338,15 @@ public class JdbcResultSetTest {
     assertTrue(subject.wasNull());
   }
 
+  @Test
+  public void testGetJsonIndex() throws SQLException {
+    assertNotNull(subject.getString(JSON_COLINDEX_NOT_NULL));
+    assertEquals(JSON_VALUE, subject.getString(JSON_COLINDEX_NOT_NULL));
+    assertFalse(subject.wasNull());
+    assertNull(subject.getString(JSON_COLINDEX_NULL));
+    assertTrue(subject.wasNull());
+  }
+
   @Test
   public void testGetStringIndexForBool() throws SQLException {
     assertNotNull(subject.getString(BOOLEAN_COLINDEX_NOTNULL));
@@ -1344,6 +1369,15 @@ public class JdbcResultSetTest {
     assertTrue(subject.wasNull());
   }
 
+  @Test
+  public void testGetJsonAsObjectIndex() throws SQLException {
+    assertNotNull(subject.getObject(JSON_COLINDEX_NOT_NULL));
+    assertEquals(JSON_VALUE, subject.getObject(JSON_COLINDEX_NOT_NULL));
+    assertFalse(subject.wasNull());
+    assertNull(subject.getObject(JSON_COLINDEX_NULL));
+    assertTrue(subject.wasNull());
+  }
+
   @SuppressWarnings("deprecation")
   @Test
   public void testGetObjectLabelMap() throws SQLException {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
index 19f760b..1bb0e37 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java
@@ -70,6 +70,7 @@ public class JdbcTypeConverterTest {
           Type.float64(),
           Type.int64(),
           Type.string(),
+          Type.json(),
           Type.timestamp(),
           Type.numeric()
         }) {
@@ -126,6 +127,23 @@ public class JdbcTypeConverterTest {
     assertThat(convert(testValues, Type.bytes(), String.class)).isEqualTo("test");
   }
 
+  @Test
+  public void testConvertJson() throws SQLException {
+    String testValue = "{\"test\": foo}";
+    assertConvertThrows(testValue, Type.json(), Boolean.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Byte.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Short.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Integer.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Long.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Float.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), Double.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), BigInteger.class, Code.INVALID_ARGUMENT);
+    assertConvertThrows(testValue, Type.json(), BigDecimal.class, Code.INVALID_ARGUMENT);
+
+    assertThat(convert(testValue, Type.json(), byte[].class)).isEqualTo(testValue.getBytes(UTF8));
+    assertThat(convert(testValue, Type.json(), String.class)).isEqualTo(testValue);
+  }
+
   private TimeZone[] getTestTimeZones() {
     return new TimeZone[] {
       TimeZone.getTimeZone("GMT-12:00"),
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
index 7555399..087d688 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java
@@ -122,6 +122,7 @@ public class ITJdbcDatabaseMetaDataTest extends ITAbstractJdbcTest {
           new Column("ColTimestamp", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null),
           new Column("ColCommitTS", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null),
           new Column("ColNumeric", Types.NUMERIC, "NUMERIC", 15, null, 10, false, null),
+          new Column("ColJson", Types.NVARCHAR, "JSON", 2621440, null, null, false, 2621440),
           new Column("ColInt64Array", Types.ARRAY, "ARRAY<INT64>", 19, null, 10, true, null),
           new Column("ColFloat64Array", Types.ARRAY, "ARRAY<FLOAT64>", 15, 16, 2, true, null),
           new Column("ColBoolArray", Types.ARRAY, "ARRAY<BOOL>", null, null, null, true, null),
@@ -151,6 +152,8 @@ public class ITJdbcDatabaseMetaDataTest extends ITAbstractJdbcTest {
           new Column(
               "ColTimestampArray", Types.ARRAY, "ARRAY<TIMESTAMP>", 35, null, null, true, null),
           new Column("ColNumericArray", Types.ARRAY, "ARRAY<NUMERIC>", 15, null, 10, true, null),
+          new Column(
+              "ColJsonArray", Types.ARRAY, "ARRAY<JSON>", 2621440, null, null, true, 2621440),
           new Column(
               "ColComputed",
               Types.NVARCHAR,
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
index d52e560..ea1bbfb 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java
@@ -30,6 +30,8 @@ import com.google.cloud.ByteArray;
 import com.google.cloud.spanner.IntegrationTest;
 import com.google.cloud.spanner.Value;
 import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest;
+import com.google.cloud.spanner.jdbc.JsonType;
+import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
 import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
 import java.io.File;
@@ -748,84 +750,113 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
 
   @Test
   public void test08_InsertAllColumnTypes() throws SQLException {
+    String sql;
+    if (EmulatorSpannerHelper.isUsingEmulator()) {
+      sql =
+          "INSERT INTO TableWithAllColumnTypes ("
+              + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, "
+              + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray"
+              + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+    } else {
+      sql =
+          "INSERT INTO TableWithAllColumnTypes ("
+              + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, ColJson, "
+              + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray"
+              + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+    }
     try (Connection con = createConnection()) {
-      try (PreparedStatement ps =
-          con.prepareStatement(
-              "INSERT INTO TableWithAllColumnTypes ("
-                  + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, "
-                  + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray"
-                  + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
-        ps.setLong(1, 1L);
-        ps.setDouble(2, 2D);
-        ps.setBoolean(3, true);
-        ps.setString(4, "test");
-        ps.setObject(5, UUID.fromString("2d37f522-e0a5-4f22-8e09-4d77d299c967"));
-        ps.setBytes(6, "test".getBytes());
-        ps.setBytes(7, "testtest".getBytes());
-        ps.setDate(8, new Date(System.currentTimeMillis()));
-        ps.setTimestamp(9, new Timestamp(System.currentTimeMillis()));
-        ps.setBigDecimal(10, BigDecimal.TEN);
-        ps.setArray(11, con.createArrayOf("INT64", new Long[] {1L, 2L, 3L}));
-        ps.setArray(12, con.createArrayOf("FLOAT64", new Double[] {1.1D, 2.2D, 3.3D}));
+      try (PreparedStatement ps = con.prepareStatement(sql)) {
+        int index = 0;
+        ps.setLong(++index, 1L);
+        ps.setDouble(++index, 2D);
+        ps.setBoolean(++index, true);
+        ps.setString(++index, "test");
+        ps.setObject(++index, UUID.fromString("2d37f522-e0a5-4f22-8e09-4d77d299c967"));
+        ps.setBytes(++index, "test".getBytes());
+        ps.setBytes(++index, "testtest".getBytes());
+        ps.setDate(++index, new Date(System.currentTimeMillis()));
+        ps.setTimestamp(++index, new Timestamp(System.currentTimeMillis()));
+        ps.setBigDecimal(++index, BigDecimal.TEN);
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          ps.setObject(++index, "{\"test_value\": \"foo\"}", JsonType.INSTANCE);
+        }
+        ps.setArray(++index, con.createArrayOf("INT64", new Long[] {1L, 2L, 3L}));
+        ps.setArray(++index, con.createArrayOf("FLOAT64", new Double[] {1.1D, 2.2D, 3.3D}));
         ps.setArray(
-            13, con.createArrayOf("BOOL", new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}));
-        ps.setArray(14, con.createArrayOf("STRING", new String[] {"1", "2", "3"}));
-        ps.setArray(15, con.createArrayOf("STRING", new String[] {"3", "2", "1"}));
+            ++index, con.createArrayOf("BOOL", new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}));
+        ps.setArray(++index, con.createArrayOf("STRING", new String[] {"1", "2", "3"}));
+        ps.setArray(++index, con.createArrayOf("STRING", new String[] {"3", "2", "1"}));
         ps.setArray(
-            16,
+            ++index,
             con.createArrayOf(
                 "BYTES", new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()}));
         ps.setArray(
-            17,
+            ++index,
             con.createArrayOf(
                 "BYTES", new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()}));
         ps.setArray(
-            18,
+            ++index,
             con.createArrayOf(
                 "DATE", new Date[] {new Date(System.currentTimeMillis()), null, new Date(0)}));
         ps.setArray(
-            19,
+            ++index,
             con.createArrayOf(
                 "TIMESTAMP",
                 new Timestamp[] {
                   new Timestamp(System.currentTimeMillis()), null, new Timestamp(0)
                 }));
         ps.setArray(
-            20,
+            ++index,
             con.createArrayOf("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN}));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          ps.setArray(
+              ++index,
+              con.createArrayOf(
+                  "JSON", new String[] {"{\"test_value\": \"foo\"}", "{}", "[]", null}));
+        }
         assertEquals(1, ps.executeUpdate());
       }
       try (ResultSet rs =
           con.createStatement().executeQuery("SELECT * FROM TableWithAllColumnTypes")) {
+        int index = 0;
         assertTrue(rs.next());
-        assertEquals(1L, rs.getLong(1));
-        assertEquals(2d, rs.getDouble(2), 0.0d);
-        assertTrue(rs.getBoolean(3));
-        assertEquals("test", rs.getString(4));
-        assertEquals("2d37f522-e0a5-4f22-8e09-4d77d299c967", rs.getString(5));
-        assertArrayEquals("test".getBytes(), rs.getBytes(6));
-        assertArrayEquals("testtest".getBytes(), rs.getBytes(7));
-        assertNotNull(rs.getDate(8));
-        assertNotNull(rs.getTimestamp(9));
-        assertNotNull(rs.getTime(10)); // Commit timestamp
-        assertEquals(BigDecimal.TEN, rs.getBigDecimal(11));
-        assertArrayEquals(new Long[] {1L, 2L, 3L}, (Long[]) rs.getArray(12).getArray());
-        assertArrayEquals(new Double[] {1.1D, 2.2D, 3.3D}, (Double[]) rs.getArray(13).getArray());
+        assertEquals(1L, rs.getLong(++index));
+        assertEquals(2d, rs.getDouble(++index), 0.0d);
+        assertTrue(rs.getBoolean(++index));
+        assertEquals("test", rs.getString(++index));
+        assertEquals("2d37f522-e0a5-4f22-8e09-4d77d299c967", rs.getString(++index));
+        assertArrayEquals("test".getBytes(), rs.getBytes(++index));
+        assertArrayEquals("testtest".getBytes(), rs.getBytes(++index));
+        assertNotNull(rs.getDate(++index));
+        assertNotNull(rs.getTimestamp(++index));
+        assertNotNull(rs.getTime(++index)); // Commit timestamp
+        assertEquals(BigDecimal.TEN, rs.getBigDecimal(++index));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          assertEquals("{\"test_value\":\"foo\"}", rs.getString(++index));
+        }
+        assertArrayEquals(new Long[] {1L, 2L, 3L}, (Long[]) rs.getArray(++index).getArray());
+        assertArrayEquals(
+            new Double[] {1.1D, 2.2D, 3.3D}, (Double[]) rs.getArray(++index).getArray());
         assertArrayEquals(
-            new Boolean[] {true, null, false}, (Boolean[]) rs.getArray(14).getArray());
-        assertArrayEquals(new String[] {"1", "2", "3"}, (String[]) rs.getArray(15).getArray());
-        assertArrayEquals(new String[] {"3", "2", "1"}, (String[]) rs.getArray(16).getArray());
+            new Boolean[] {true, null, false}, (Boolean[]) rs.getArray(++index).getArray());
+        assertArrayEquals(new String[] {"1", "2", "3"}, (String[]) rs.getArray(++index).getArray());
+        assertArrayEquals(new String[] {"3", "2", "1"}, (String[]) rs.getArray(++index).getArray());
         assertArrayEquals(
             new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()},
-            (byte[][]) rs.getArray(17).getArray());
+            (byte[][]) rs.getArray(++index).getArray());
         assertArrayEquals(
             new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()},
-            (byte[][]) rs.getArray(18).getArray());
-        assertEquals(3, ((Date[]) rs.getArray(19).getArray()).length);
-        assertEquals(3, ((Timestamp[]) rs.getArray(20).getArray()).length);
+            (byte[][]) rs.getArray(++index).getArray());
+        assertEquals(3, ((Date[]) rs.getArray(++index).getArray()).length);
+        assertEquals(3, ((Timestamp[]) rs.getArray(++index).getArray()).length);
         assertArrayEquals(
             new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN},
-            (BigDecimal[]) rs.getArray(21).getArray());
+            (BigDecimal[]) rs.getArray(++index).getArray());
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          assertArrayEquals(
+              new String[] {"{\"test_value\":\"foo\"}", "{}", "[]", null},
+              (String[]) rs.getArray(++index).getArray());
+        }
         assertFalse(rs.next());
       }
     }
@@ -838,7 +869,7 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
       try (PreparedStatement ps =
           con.prepareStatement("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=?")) {
         ResultSetMetaData metadata = ps.getMetaData();
-        assertEquals(22, metadata.getColumnCount());
+        assertEquals(24, metadata.getColumnCount());
         int index = 0;
         assertEquals("ColInt64", metadata.getColumnLabel(++index));
         assertEquals("ColFloat64", metadata.getColumnLabel(++index));
@@ -851,6 +882,7 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
         assertEquals("ColTimestamp", metadata.getColumnLabel(++index));
         assertEquals("ColCommitTS", metadata.getColumnLabel(++index));
         assertEquals("ColNumeric", metadata.getColumnLabel(++index));
+        assertEquals("ColJson", metadata.getColumnLabel(++index));
         assertEquals("ColInt64Array", metadata.getColumnLabel(++index));
         assertEquals("ColFloat64Array", metadata.getColumnLabel(++index));
         assertEquals("ColBoolArray", metadata.getColumnLabel(++index));
@@ -861,6 +893,7 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
         assertEquals("ColDateArray", metadata.getColumnLabel(++index));
         assertEquals("ColTimestampArray", metadata.getColumnLabel(++index));
         assertEquals("ColNumericArray", metadata.getColumnLabel(++index));
+        assertEquals("ColJsonArray", metadata.getColumnLabel(++index));
         assertEquals("ColComputed", metadata.getColumnLabel(++index));
       }
     }
@@ -880,104 +913,135 @@ public class ITJdbcPreparedStatementTest extends ITAbstractJdbcTest {
 
   @Test
   public void test11_InsertDataUsingSpannerValue() throws SQLException {
+    String sql;
+    if (EmulatorSpannerHelper.isUsingEmulator()) {
+      sql =
+          "INSERT INTO TableWithAllColumnTypes ("
+              + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, "
+              + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray"
+              + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+    } else {
+      sql =
+          "INSERT INTO TableWithAllColumnTypes ("
+              + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, ColJson, "
+              + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray"
+              + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+    }
     try (Connection con = createConnection()) {
-      try (PreparedStatement ps =
-          con.prepareStatement(
-              "INSERT INTO TableWithAllColumnTypes ("
-                  + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, "
-                  + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray"
-                  + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) {
-        ps.setObject(1, Value.int64(2L));
-        ps.setObject(2, Value.float64(2D));
-        ps.setObject(3, Value.bool(true));
-        ps.setObject(4, Value.string("testvalues"));
-        ps.setObject(5, Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"));
-        ps.setObject(6, Value.bytes(ByteArray.copyFrom("test".getBytes())));
-        ps.setObject(7, Value.bytes(ByteArray.copyFrom("testtest".getBytes())));
-        ps.setObject(8, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
+      try (PreparedStatement ps = con.prepareStatement(sql)) {
+        int index = 1;
+        ps.setObject(index++, Value.int64(2L));
+        ps.setObject(index++, Value.float64(2D));
+        ps.setObject(index++, Value.bool(true));
+        ps.setObject(index++, Value.string("testvalues"));
+        ps.setObject(index++, Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"));
+        ps.setObject(index++, Value.bytes(ByteArray.copyFrom("test".getBytes())));
+        ps.setObject(index++, Value.bytes(ByteArray.copyFrom("testtest".getBytes())));
+        ps.setObject(index++, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)));
         ps.setObject(
-            9, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99)));
-        ps.setObject(10, Value.numeric(BigDecimal.TEN));
-        ps.setObject(11, Value.int64Array(new long[] {1L, 2L, 3L}));
-        ps.setObject(12, Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}));
-        ps.setObject(13, Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE)));
-        ps.setObject(14, Value.stringArray(Arrays.asList("1", "2", "3")));
-        ps.setObject(15, Value.stringArray(Arrays.asList("3", "2", "1")));
+            index++, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99)));
+        ps.setObject(index++, Value.numeric(BigDecimal.TEN));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          ps.setObject(index++, Value.json("{\"test_value\": \"foo\"}"));
+        }
+        ps.setObject(index++, Value.int64Array(new long[] {1L, 2L, 3L}));
+        ps.setObject(index++, Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}));
+        ps.setObject(index++, Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE)));
+        ps.setObject(index++, Value.stringArray(Arrays.asList("1", "2", "3")));
+        ps.setObject(index++, Value.stringArray(Arrays.asList("3", "2", "1")));
         ps.setObject(
-            16,
+            index++,
             Value.bytesArray(
                 Arrays.asList(
                     ByteArray.copyFrom("1"), ByteArray.copyFrom("2"), ByteArray.copyFrom("3"))));
         ps.setObject(
-            17,
+            index++,
             Value.bytesArray(
                 Arrays.asList(
                     ByteArray.copyFrom("333"),
                     ByteArray.copyFrom("222"),
                     ByteArray.copyFrom("111"))));
         ps.setObject(
-            18,
+            index++,
             Value.dateArray(
                 Arrays.asList(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3), null)));
         ps.setObject(
-            19,
+            index++,
             Value.timestampArray(
                 Arrays.asList(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99), null)));
-        ps.setObject(20, Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)));
+        ps.setObject(
+            index++, Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          ps.setObject(
+              index++,
+              Value.jsonArray(Arrays.asList("{\"key1\": \"val1\"}", null, "{\"key2\": \"val2\"}")));
+        }
         assertEquals(1, ps.executeUpdate());
       }
       try (ResultSet rs =
           con.createStatement()
               .executeQuery("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=2")) {
         assertTrue(rs.next());
-        assertEquals(Value.int64(2L), rs.getObject(1, Value.class));
-        assertEquals(Value.float64(2d), rs.getObject(2, Value.class));
-        assertEquals(Value.bool(true), rs.getObject(3, Value.class));
-        assertEquals(Value.string("testvalues"), rs.getObject(4, Value.class));
+        int index = 1;
+        assertEquals(Value.int64(2L), rs.getObject(index++, Value.class));
+        assertEquals(Value.float64(2d), rs.getObject(index++, Value.class));
+        assertEquals(Value.bool(true), rs.getObject(index++, Value.class));
+        assertEquals(Value.string("testvalues"), rs.getObject(index++, Value.class));
+        assertEquals(
+            Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"),
+            rs.getObject(index++, Value.class));
+        assertEquals(Value.bytes(ByteArray.copyFrom("test")), rs.getObject(index++, Value.class));
         assertEquals(
-            Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"), rs.getObject(5, Value.class));
-        assertEquals(Value.bytes(ByteArray.copyFrom("test")), rs.getObject(6, Value.class));
-        assertEquals(Value.bytes(ByteArray.copyFrom("testtest")), rs.getObject(7, Value.class));
+            Value.bytes(ByteArray.copyFrom("testtest")), rs.getObject(index++, Value.class));
         assertEquals(
             Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)),
-            rs.getObject(8, Value.class));
+            rs.getObject(index++, Value.class));
         assertEquals(
             Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99)),
-            rs.getObject(9, Value.class));
-        assertNotNull(rs.getObject(10, Value.class)); // Commit timestamp
-        assertEquals(Value.numeric(BigDecimal.TEN), rs.getObject(11, Value.class));
-        assertEquals(Value.int64Array(new long[] {1L, 2L, 3L}), rs.getObject(12, Value.class));
+            rs.getObject(index++, Value.class));
+        assertNotNull(rs.getObject(index++, Value.class)); // Commit timestamp
+        assertEquals(Value.numeric(BigDecimal.TEN), rs.getObject(index++, Value.class));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          assertEquals(Value.json("{\"test_value\":\"foo\"}"), rs.getObject(index++, Value.class));
+        }
+        assertEquals(Value.int64Array(new long[] {1L, 2L, 3L}), rs.getObject(index++, Value.class));
         assertEquals(
-            Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}), rs.getObject(13, Value.class));
+            Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}),
+            rs.getObject(index++, Value.class));
         assertEquals(
-            Value.boolArray(Arrays.asList(true, null, false)), rs.getObject(14, Value.class));
+            Value.boolArray(Arrays.asList(true, null, false)), rs.getObject(index++, Value.class));
         assertEquals(
-            Value.stringArray(Arrays.asList("1", "2", "3")), rs.getObject(15, Value.class));
+            Value.stringArray(Arrays.asList("1", "2", "3")), rs.getObject(index++, Value.class));
         assertEquals(
-            Value.stringArray(Arrays.asList("3", "2", "1")), rs.getObject(16, Value.class));
+            Value.stringArray(Arrays.asList("3", "2", "1")), rs.getObject(index++, Value.class));
         assertEquals(
             Value.bytesArray(
                 Arrays.asList(
                     ByteArray.copyFrom("1"), ByteArray.copyFrom("2"), ByteArray.copyFrom("3"))),
-            rs.getObject(17, Value.class));
+            rs.getObject(index++, Value.class));
         assertEquals(
             Value.bytesArray(
                 Arrays.asList(
                     ByteArray.copyFrom("333"),
                     ByteArray.copyFrom("222"),
                     ByteArray.copyFrom("111"))),
-            rs.getObject(18, Value.class));
+            rs.getObject(index++, Value.class));
         assertEquals(
             Value.dateArray(
                 Arrays.asList(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3), null)),
-            rs.getObject(19, Value.class));
+            rs.getObject(index++, Value.class));
         assertEquals(
             Value.timestampArray(
                 Arrays.asList(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99), null)),
-            rs.getObject(20, Value.class));
+            rs.getObject(index++, Value.class));
         assertEquals(
             Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)),
-            rs.getObject(21, Value.class));
+            rs.getObject(index++, Value.class));
+        if (!EmulatorSpannerHelper.isUsingEmulator()) {
+          assertEquals(
+              Value.jsonArray(Arrays.asList("{\"key1\":\"val1\"}", null, "{\"key2\":\"val2\"}")),
+              rs.getObject(index++, Value.class));
+        }
         assertFalse(rs.next());
       }
     }
diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
index 6a8c461..9b2cb3b 100644
--- a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
+++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql
@@ -74,6 +74,7 @@ CREATE TABLE TableWithAllColumnTypes (
   ColTimestamp	TIMESTAMP	NOT NULL,
   ColCommitTS	TIMESTAMP	NOT NULL OPTIONS (allow_commit_timestamp=true),
   ColNumeric	NUMERIC		NOT NULL,
+  ColJson		JSON		NOT NULL,
   
   ColInt64Array		ARRAY<INT64>,
   ColFloat64Array	ARRAY<FLOAT64>,
@@ -85,6 +86,7 @@ CREATE TABLE TableWithAllColumnTypes (
   ColDateArray		ARRAY<DATE>,
   ColTimestampArray	ARRAY<TIMESTAMP>,
   ColNumericArray	ARRAY<NUMERIC>,
+  ColJsonArray		ARRAY<JSON>,
   
   ColComputed STRING(MAX) AS (CONCAT(COALESCE(ColString, ''), ' ', COALESCE(ColStringMax, ''))) STORED,
 ) PRIMARY KEY (ColInt64)
diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql
new file mode 100644
index 0000000..d1e73bb
--- /dev/null
+++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed 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
+ *
+ *       http://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.
+ */
+
+START BATCH DDL;
+
+CREATE TABLE Singers (
+  SingerId   INT64 NOT NULL,
+  FirstName  STRING(1024),
+  LastName   STRING(1024),
+  SingerInfo BYTES(MAX),
+  BirthDate  DATE
+) PRIMARY KEY(SingerId);
+
+CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);
+
+CREATE TABLE Albums (
+  SingerId        INT64 NOT NULL,
+  AlbumId         INT64 NOT NULL,
+  AlbumTitle      STRING(MAX),
+  MarketingBudget INT64
+) PRIMARY KEY(SingerId, AlbumId),
+  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;
+
+CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);
+
+CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);
+
+CREATE TABLE Songs (
+  SingerId  INT64 NOT NULL,
+  AlbumId   INT64 NOT NULL,
+  TrackId   INT64 NOT NULL,
+  SongName  STRING(MAX),
+  Duration  INT64,
+  SongGenre STRING(25)
+) PRIMARY KEY(SingerId, AlbumId, TrackId),
+  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;
+
+CREATE UNIQUE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC), INTERLEAVE IN Albums;
+
+CREATE INDEX SongsBySongName ON Songs(SongName);
+
+CREATE TABLE Concerts (
+  VenueId      INT64 NOT NULL,
+  SingerId     INT64 NOT NULL,
+  ConcertDate  DATE NOT NULL,
+  BeginTime    TIMESTAMP,
+  EndTime      TIMESTAMP,
+  TicketPrices ARRAY<INT64>,
+  CONSTRAINT Fk_Concerts_Singer FOREIGN KEY (SingerId) REFERENCES Singers (SingerId)
+) PRIMARY KEY(VenueId, SingerId, ConcertDate);
+
+CREATE TABLE TableWithAllColumnTypes (
+  ColInt64		INT64		NOT NULL,
+  ColFloat64	FLOAT64		NOT NULL,
+  ColBool		BOOL		NOT NULL,
+  ColString		STRING(100) NOT NULL,
+  ColStringMax	STRING(MAX)	NOT NULL,
+  ColBytes		BYTES(100)	NOT NULL,
+  ColBytesMax	BYTES(MAX)	NOT NULL,
+  ColDate		DATE		NOT NULL,
+  ColTimestamp	TIMESTAMP	NOT NULL,
+  ColCommitTS	TIMESTAMP	NOT NULL OPTIONS (allow_commit_timestamp=true),
+  ColNumeric	NUMERIC		NOT NULL,
+
+  ColInt64Array		ARRAY<INT64>,
+  ColFloat64Array	ARRAY<FLOAT64>,
+  ColBoolArray		ARRAY<BOOL>,
+  ColStringArray	ARRAY<STRING(100)>,
+  ColStringMaxArray	ARRAY<STRING(MAX)>,
+  ColBytesArray		ARRAY<BYTES(100)>,
+  ColBytesMaxArray	ARRAY<BYTES(MAX)>,
+  ColDateArray		ARRAY<DATE>,
+  ColTimestampArray	ARRAY<TIMESTAMP>,
+  ColNumericArray	ARRAY<NUMERIC>,
+
+  ColComputed STRING(MAX) AS (CONCAT(COALESCE(ColString, ''), ' ', COALESCE(ColStringMax, ''))) STORED,
+) PRIMARY KEY (ColInt64)
+;
+
+CREATE TABLE TableWithRef (
+  Id          INT64       NOT NULL,
+  RefFloat    FLOAT64     NOT NULL,
+  RefString   STRING(100) NOT NULL,
+  RefDate     DATE        NOT NULL,
+  CONSTRAINT Fk_TableWithRef_TableWithAllColumnTypes
+    FOREIGN KEY (RefFloat, RefString, RefDate)
+    REFERENCES TableWithAllColumnTypes (ColFloat64, ColString, ColDate)
+) PRIMARY KEY (Id)
+;
+
+RUN BATCH;
\ No newline at end of file