Repository: googleapis/google-cloud-ruby (Ruby)

Issue:
BigQuery: support for JSON data type
I'm attempting to create a table that has a column with a data type of JSON but I noticed that JSON doesn't look to be supported with this gem as a data type. Is someone already working on adding JSON support or open to a PR? It doesn't look like this is an automated change based on this [issue](https://github.com/googleapis/google-cloud-ruby/issues/10094) and related [PR](https://github.com/googleapis/google-cloud-ruby/pull/10878) for BIGNUMERIC.

I initially noticed this when attempting to call `schema.json` which failed with an undefined method error and then realized it's also not listed in the `TYPES` constant within the [Schema::Field](url) class.

https://github.com/googleapis/google-cloud-ruby/blob/cdd40a1ba7cef5a09e777076eb245702fadffa3b/google-cloud-bigquery/lib/google/cloud/bigquery/schema/field.rb#L43

``` ruby
table = bq_dataset.create_table "test" do |schema|
  schema.json :data
end
```
```
NoMethodError: undefined method `json' for #<Google::Cloud::Bigquery::Table::Updater:0x00007f8a14e830c8>
```


Discussion:
Tracked internally as a feature request in b/256677227 
Is there a workaround for this? I'm having the same issue.
@SandeepTuniki - Can you look into this to see if there is a workaround? 

@alanhala - This is being tracked internally but there is no ETA for this yet. Will update this thread once we have more clear timeline. 
I'm using `schema.load(File.read("schema.json"))` as a workaround.
Any updates?
@NivedhaSenthil This is huge, thanks so much!



Patch:
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/convert.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/convert.rb
index 3b0ce7da36b8..eec0df46870e 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/convert.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/convert.rb
@@ -39,6 +39,7 @@ module Bigquery
       #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
       #   | `DATE`       | `Date`                               |                                                    |
       #   | `GEOGRAPHY`  | `String`                             |                                                    |
+      #   | `JSON`       | `String`                             | String, as JSON does not have a schema to verify.  |
       #   | `TIMESTAMP`  | `Time`                               |                                                    |
       #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
       #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -72,7 +73,7 @@ def self.format_value value, field
             value[:v].map { |v| format_value v, field }
           elsif Hash === value[:v]
             format_row value[:v], field.fields
-          elsif field.type == "STRING"
+          elsif field.type == "STRING" || field.type == "JSON" || field.type == "GEOGRAPHY"
             String value[:v]
           elsif field.type == "INTEGER"
             Integer value[:v]
@@ -102,8 +103,6 @@ def self.format_value value, field
             ::Time.parse("#{value[:v]} UTC").to_datetime
           elsif field.type == "DATE"
             Date.parse value[:v]
-          elsif field.type == "GEOGRAPHY"
-            String value[:v]
           else
             value[:v]
           end
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb
index 2338f57eb3fa..8370ee59506a 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb
@@ -1180,6 +1180,7 @@ def routines token: nil, max: nil, filter: nil
         #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         #   | `DATE`       | `Date`                               |                                                    |
         #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`, below.|
+        #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         #   | `TIMESTAMP`  | `Time`                               |                                                    |
         #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -1206,6 +1207,7 @@ def routines token: nil, max: nil, filter: nil
         #   * `:DATETIME`
         #   * `:DATE`
         #   * `:GEOGRAPHY`
+        #   * `:JSON`
         #   * `:TIMESTAMP`
         #   * `:TIME`
         #   * `:BYTES`
@@ -1585,6 +1587,7 @@ def query_job query,
         #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         #   | `DATE`       | `Date`                               |                                                    |
         #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`, below.|
+        #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         #   | `TIMESTAMP`  | `Time`                               |                                                    |
         #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -1611,6 +1614,7 @@ def query_job query,
         #   * `:DATETIME`
         #   * `:DATE`
         #   * `:GEOGRAPHY`
+        #   * `:JSON`
         #   * `:TIMESTAMP`
         #   * `:TIME`
         #   * `:BYTES`
@@ -2553,6 +2557,7 @@ def self.new_reference project_id, dataset_id, service
         # | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         # | `DATE`       | `Date`                               |                                                    |
         # | `GEOGRAPHY`  | `String`                             |                                                    |
+        # | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         # | `TIMESTAMP`  | `Time`                               |                                                    |
         # | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         # | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb
index 5355a0038b19..dba4cffcc099 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb
@@ -1531,6 +1531,71 @@ def geography name, description: nil, mode: :nullable, policy_tags: nil,
                              default_value_expression: default_value_expression
           end
 
+          ##
+          # Adds an json field to the schema.
+          #
+          # See {Schema#json}.
+          # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#json_type
+          #
+          # @param [String] name The field name. The name must contain only
+          #   letters (`[A-Za-z]`), numbers (`[0-9]`), or underscores (`_`), and must
+          #   start with a letter or underscore. The maximum length is 128
+          #   characters.
+          # @param [String] description A description of the field.
+          # @param [Symbol] mode The field's mode. The possible values are
+          #   `:nullable`, `:required`, and `:repeated`. The default value is
+          #   `:nullable`.
+          # @param [Array<String>, String] policy_tags The policy tag list or
+          #   single policy tag for the field. Policy tag identifiers are of
+          #   the form `projects/*/locations/*/taxonomies/*/policyTags/*`.
+          #   At most 1 policy tag is currently allowed.
+          # @param default_value_expression [String] The default value of a field
+          #   using a SQL expression. It can only be set for top level fields (columns).
+          #   Use a struct or array expression to specify default value for the entire struct or
+          #   array. The valid SQL expressions are:
+          #     - Literals for all data types, including STRUCT and ARRAY.
+          #     - The following functions:
+          #         `CURRENT_TIMESTAMP`
+          #         `CURRENT_TIME`
+          #         `CURRENT_DATE`
+          #         `CURRENT_DATETIME`
+          #         `GENERATE_UUID`
+          #         `RAND`
+          #         `SESSION_USER`
+          #         `ST_GEOPOINT`
+          #     - Struct or array composed with the above allowed functions, for example:
+          #         "[CURRENT_DATE(), DATE '2020-01-01'"]
+          #
+          # @example
+          #   require "google/cloud/bigquery"
+          #
+          #   bigquery = Google::Cloud::Bigquery.new
+          #   dataset = bigquery.dataset "my_dataset"
+          #   job = dataset.load_job "my_table", "gs://abc/file" do |schema|
+          #     schema.record "cities_lived", mode: :repeated do |cities_lived|
+          #       cities_lived.geography "location", mode: :required
+          #       cities_lived.integer "number_of_years", mode: :required
+          #       cities_lived.json "address", mode: :required
+          #     end
+          #   end
+          #
+          # @example Add field with default value.
+          #   require "google/cloud/bigquery"
+          #
+          #   bigquery = Google::Cloud::Bigquery.new
+          #   dataset = bigquery.dataset "my_dataset"
+          #   job = dataset.load_job "my_table", "gs://abc/file" do |schema|
+          #     schema.json "person", default_value_expression: "JSON '{"name": "Alice", "age": 30}'"
+          #   end
+          #
+          # @!group Schema
+          #
+          def json name, description: nil, mode: :nullable, policy_tags: nil,
+                   default_value_expression: nil
+            schema.json name, description: description, mode: mode, policy_tags: policy_tags,
+                        default_value_expression: default_value_expression
+          end
+
           ##
           # Adds a record field to the schema. A block must be passed describing
           # the nested fields of the record. For more information about nested
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb
index 70f7544b33cd..562876a1e272 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb
@@ -304,6 +304,7 @@ def copy source_table, destination_table, create: nil, write: nil, &block
         #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         #   | `DATE`       | `Date`                               |                                                    |
         #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`, below.|
+        #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         #   | `TIMESTAMP`  | `Time`                               |                                                    |
         #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -330,6 +331,7 @@ def copy source_table, destination_table, create: nil, write: nil, &block
         #   * `:DATETIME`
         #   * `:DATE`
         #   * `:GEOGRAPHY`
+        #   * `:JSON`
         #   * `:TIMESTAMP`
         #   * `:TIME`
         #   * `:BYTES`
@@ -691,6 +693,7 @@ def query_job query,
         #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         #   | `DATE`       | `Date`                               |                                                    |
         #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`, below.|
+        #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         #   | `TIMESTAMP`  | `Time`                               |                                                    |
         #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -717,6 +720,7 @@ def query_job query,
         #   * `:DATETIME`
         #   * `:DATE`
         #   * `:GEOGRAPHY`
+        #   * `:JSON`
         #   * `:TIMESTAMP`
         #   * `:TIME`
         #   * `:BYTES`
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb
index 6ee838f0a7b3..683c846d5f96 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb
@@ -927,6 +927,7 @@ def dataset= value
           #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.           |
           #   | `DATE`       | `Date`                               |                                                  |
           #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`.     |
+          #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.|
           #   | `TIMESTAMP`  | `Time`                               |                                                  |
           #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                  |
           #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                  |
@@ -964,6 +965,7 @@ def params= params
           #   | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.           |
           #   | `DATE`       | `Date`                               |                                                  |
           #   | `GEOGRAPHY`  | `String` (WKT or GeoJSON)            | NOT AUTOMATIC: Must be mapped using `types`.     |
+          #   | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.|
           #   | `TIMESTAMP`  | `Time`                               |                                                  |
           #   | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                  |
           #   | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                  |
@@ -990,6 +992,7 @@ def params= params
           #   * `:DATETIME`
           #   * `:DATE`
           #   * `:GEOGRAPHY`
+          #   * `:JSON`
           #   * `:TIMESTAMP`
           #   * `:TIME`
           #   * `:BYTES`
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/schema.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/schema.rb
index 78dd553d01a7..57570e449496 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/schema.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/schema.rb
@@ -829,6 +829,49 @@ def geography name, description: nil, mode: :nullable,
                     default_value_expression: default_value_expression
         end
 
+        ##
+        # Adds a JSON field to the schema.
+        #
+        # @see https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#json_type
+        #
+        # @param [String] name The field name. The name must contain only
+        #   letters (`[A-Za-z]`), numbers (`[0-9]`), or underscores (`_`), and must
+        #   start with a letter or underscore. The maximum length is 128
+        #   characters.
+        # @param [String] description A description of the field.
+        # @param [Symbol] mode The field's mode. The possible values are
+        #   `:nullable`, `:required`, and `:repeated`. The default value is
+        #   `:nullable`.
+        # @param [Array<String>, String] policy_tags The policy tag list or
+        #   single policy tag for the field. Policy tag identifiers are of
+        #   the form `projects/*/locations/*/taxonomies/*/policyTags/*`.
+        #   At most 1 policy tag is currently allowed.
+        # @param default_value_expression [String] The default value of a field
+        #   using a SQL expression. It can only be set for top level fields (columns).
+        #   Use a struct or array expression to specify default value for the entire struct or
+        #   array. The valid SQL expressions are:
+        #     - Literals for all data types, including STRUCT and ARRAY.
+        #     - The following functions:
+        #         `CURRENT_TIMESTAMP`
+        #         `CURRENT_TIME`
+        #         `CURRENT_DATE`
+        #         `CURRENT_DATETIME`
+        #         `GENERATE_UUID`
+        #         `RAND`
+        #         `SESSION_USER`
+        #         `ST_GEOPOINT`
+        #     - Struct or array composed with the above allowed functions, for example:
+        #         "[CURRENT_DATE(), DATE '2020-01-01'"]
+        #
+        def json name, description: nil, mode: :nullable,
+                 policy_tags: nil, default_value_expression: nil
+          add_field name, :json,
+                    description: description,
+                    mode: mode,
+                    policy_tags: policy_tags,
+                    default_value_expression: default_value_expression
+        end
+
         ##
         # Adds a record field to the schema. A block must be passed describing
         # the nested fields of the record. For more information about nested
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/schema/field.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/schema/field.rb
index 3c8b69c3b92d..227042cf6862 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/schema/field.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/schema/field.rb
@@ -52,6 +52,7 @@ class Field
             "GEOGRAPHY",
             "INTEGER",
             "INT64",
+            "JSON",
             "NUMERIC",
             "RECORD",
             "STRING",
@@ -98,6 +99,7 @@ def name= new_name
           #   * `FLOAT`
           #   * `FLOAT64` (same as `FLOAT`)
           #   * `GEOGRAPHY`
+          #   * `JSON`
           #   * `INTEGER`
           #   * `INT64` (same as `INTEGER`)
           #   * `NUMERIC`
@@ -125,6 +127,7 @@ def type
           #   * `FLOAT`
           #   * `FLOAT64` (same as `FLOAT`)
           #   * `GEOGRAPHY`
+          #   * `JSON`
           #   * `INTEGER`
           #   * `INT64` (same as `INTEGER`)
           #   * `NUMERIC`
@@ -456,6 +459,15 @@ def geography?
             type == "GEOGRAPHY"
           end
 
+          ##
+          # Checks if the type of the field is `JSON`.
+          #
+          # @return [Boolean] `true` when `JSON`, `false` otherwise.
+          #
+          def json?
+            type == "JSON"
+          end
+
           ##
           # Checks if the type of the field is `RECORD`.
           #
@@ -893,6 +905,30 @@ def geography name, description: nil, mode: :nullable, policy_tags: nil
             add_field name, :geography, description: description, mode: mode, policy_tags: policy_tags
           end
 
+          ##
+          # Adds a json field to the nested schema of a record field.
+          #
+          # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#json_type
+          #
+          # @param [String] name The field name. The name must contain only
+          #   letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
+          #   start with a letter or underscore. The maximum length is 128
+          #   characters.
+          # @param [String] description A description of the field.
+          # @param [Symbol] mode The field's mode. The possible values are
+          #   `:nullable`, `:required`, and `:repeated`. The default value is
+          #   `:nullable`.
+          # @param [Array<String>, String] policy_tags The policy tag list or
+          #   single policy tag for the field. Policy tag identifiers are of
+          #   the form `projects/*/locations/*/taxonomies/*/policyTags/*`.
+          #   At most 1 policy tag is currently allowed.
+          #
+          def json name, description: nil, mode: :nullable, policy_tags: nil
+            record_check!
+
+            add_field name, :json, description: description, mode: mode, policy_tags: policy_tags
+          end
+
           ##
           # Adds a record field to the nested schema of a record field. A block
           # must be passed describing the nested fields of the record. For more
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb
index 93a3276f74e5..71788cceefb9 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb
@@ -2648,6 +2648,7 @@ def load files, format: nil, create: nil, write: nil, projection_fields: nil, ja
         # | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
         # | `DATE`       | `Date`                               |                                                    |
         # | `GEOGRAPHY`  | `String`                             | Well-known text (WKT) or GeoJSON.                  |
+        # | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
         # | `TIMESTAMP`  | `Time`                               |                                                    |
         # | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
         # | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |
@@ -4284,6 +4285,66 @@ def geography name, description: nil, mode: :nullable, policy_tags: nil,
                              default_value_expression: default_value_expression
           end
 
+          ##
+          # Adds an json field to the schema.
+          #
+          # See {Schema#json}.
+          # https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#json_type
+          #
+          # @param [String] name The field name. The name must contain only
+          #   letters (`[A-Za-z]`), numbers (`[0-9]`), or underscores (`_`), and must
+          #   start with a letter or underscore. The maximum length is 128
+          #   characters.
+          # @param [String] description A description of the field.
+          # @param [Symbol] mode The field's mode. The possible values are
+          #   `:nullable`, `:required`, and `:repeated`. The default value is
+          #   `:nullable`.
+          # @param [Array<String>, String] policy_tags The policy tag list or
+          #   single policy tag for the field. Policy tag identifiers are of
+          #   the form `projects/*/locations/*/taxonomies/*/policyTags/*`.
+          #   At most 1 policy tag is currently allowed.
+          # @param default_value_expression [String] The default value of a field
+          #   using a SQL expression. It can only be set for top level fields (columns).
+          #   Use a struct or array expression to specify default value for the entire struct or
+          #   array. The valid SQL expressions are:
+          #     - Literals for all data types, including STRUCT and ARRAY.
+          #     - The following functions:
+          #         `CURRENT_TIMESTAMP`
+          #         `CURRENT_TIME`
+          #         `CURRENT_DATE`
+          #         `CURRENT_DATETIME`
+          #         `GENERATE_UUID`
+          #         `RAND`
+          #         `SESSION_USER`
+          #         `ST_GEOPOINT`
+          #     - Struct or array composed with the above allowed functions, for example:
+          #         "[CURRENT_DATE(), DATE '2020-01-01'"]
+          #
+          # @example
+          #   require "google/cloud/bigquery"
+          #
+          #   bigquery = Google::Cloud::Bigquery.new
+          #   dataset = bigquery.dataset "my_dataset"
+          #   table = dataset.create_table "my_table" do |schema|
+          #     schema.json "person", mode: :required
+          #   end
+          #
+          # @example Add field with default value.
+          #   require "google/cloud/bigquery"
+          #
+          #   bigquery = Google::Cloud::Bigquery.new
+          #   dataset = bigquery.dataset "my_dataset"
+          #   table = dataset.create_table "my_table" do |schema|
+          #     schema.json "person", default_value_expression: "JSON '{"name": "Alice", "age": 30}'"
+          #   end
+          #
+          # @!group Schema
+          def json name, description: nil, mode: :nullable, policy_tags: nil,
+                   default_value_expression: nil
+            schema.json name, description: description, mode: mode, policy_tags: policy_tags,
+                        default_value_expression: default_value_expression
+          end
+
           ##
           # Adds a record field to the schema. A block must be passed describing
           # the nested fields of the record. For more information about nested
diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/table/async_inserter.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/table/async_inserter.rb
index 998cf7e64525..ae85bc1c4cc9 100644
--- a/google-cloud-bigquery/lib/google/cloud/bigquery/table/async_inserter.rb
+++ b/google-cloud-bigquery/lib/google/cloud/bigquery/table/async_inserter.rb
@@ -111,6 +111,7 @@ def initialize table, skip_invalid: nil, ignore_unknown: nil, max_bytes: 10_000_
           # | `DATETIME`   | `DateTime`                           | `DATETIME` does not support time zone.             |
           # | `DATE`       | `Date`                               |                                                    |
           # | `GEOGRAPHY`  | `String`                             |                                                    |
+          # | `JSON`       | `String` (Stringified JSON)          | String, as JSON does not have a schema to verify.  |
           # | `TIMESTAMP`  | `Time`                               |                                                    |
           # | `TIME`       | `Google::Cloud::BigQuery::Time`      |                                                    |
           # | `BYTES`      | `File`, `IO`, `StringIO`, or similar |                                                    |


Test patch:
diff --git a/google-cloud-bigquery/acceptance/bigquery/named_params_test.rb b/google-cloud-bigquery/acceptance/bigquery/named_params_test.rb
index 3882aec16ad6..1561512a6ebb 100644
--- a/google-cloud-bigquery/acceptance/bigquery/named_params_test.rb
+++ b/google-cloud-bigquery/acceptance/bigquery/named_params_test.rb
@@ -232,6 +232,31 @@
     _(rows.first[:value]).must_be_nil
   end
 
+  it "queries the data with a json parameter and json type" do
+    value = { "name" => "Alice", "age" => 30}
+    rows = bigquery.query "SELECT @value AS value", 
+                          params: { value: value.to_json},
+                          types: { value: :JSON }
+
+    _(rows.class).must_equal Google::Cloud::Bigquery::Data
+    _(rows.fields.count).must_equal 1
+    _(rows.fields.first.name).must_equal "value"
+    _(rows.fields.first).must_be :json?
+    _(rows.count).must_equal 1
+    _(JSON.parse(rows.first[:value])).must_equal value
+  end
+
+  it "queries the data with a nil parameter and json type" do
+    rows = bigquery.query "SELECT @value AS value", params: { value: nil }, types: { value: :JSON }
+
+    _(rows.class).must_equal Google::Cloud::Bigquery::Data
+    _(rows.fields.count).must_equal 1
+    _(rows.fields.first.name).must_equal "value"
+    _(rows.fields.first).must_be :json?
+    _(rows.count).must_equal 1
+    _(rows.first[:value]).must_be_nil
+  end
+
   it "queries the data with a timestamp parameter" do
     now = Time.now
     rows = bigquery.query "SELECT @value AS value", params: { value: now }
diff --git a/google-cloud-bigquery/acceptance/bigquery/positional_params_test.rb b/google-cloud-bigquery/acceptance/bigquery/positional_params_test.rb
index 74e3190dfcb2..8941d054de34 100644
--- a/google-cloud-bigquery/acceptance/bigquery/positional_params_test.rb
+++ b/google-cloud-bigquery/acceptance/bigquery/positional_params_test.rb
@@ -228,6 +228,29 @@
     _(rows.first[:value]).must_be_nil
   end
 
+  it "queries the data with json parameter and json type" do
+    value = { "name" => "Alice", "age" => 30}
+    rows = bigquery.query "SELECT ? AS value", params: [value.to_json], types: [:JSON]
+
+    _(rows.class).must_equal Google::Cloud::Bigquery::Data
+    _(rows.fields.count).must_equal 1
+    _(rows.fields.first.name).must_equal "value"
+    _(rows.fields.first).must_be :json?
+    _(rows.count).must_equal 1
+    _(JSON.parse(rows.first[:value])).must_equal value
+  end
+
+  it "queries the data with a nil parameter and json type" do
+    rows = bigquery.query "SELECT ? AS value", params: [nil], types: [:JSON]
+
+    _(rows.class).must_equal Google::Cloud::Bigquery::Data
+    _(rows.fields.count).must_equal 1
+    _(rows.fields.first.name).must_equal "value"
+    _(rows.fields.first).must_be :json?
+    _(rows.count).must_equal 1
+    _(rows.first[:value]).must_be_nil
+  end
+
   it "queries the data with a timestamp parameter" do
     now = Time.now
     rows = bigquery.query "SELECT ? AS value", params: [now]
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/convert_to_query_param_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/convert_to_query_param_test.rb
index 779924217768..750416e83baa 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/convert_to_query_param_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/convert_to_query_param_test.rb
@@ -397,6 +397,52 @@
     end
   end
 
+  describe :JSON do
+    let(:json_value) { { "name" => "Alice", "age" => 30} }
+    
+    it "does NOT identify by value" do
+      expected = Google::Apis::BigqueryV2::QueryParameter.new(
+        parameter_type: Google::Apis::BigqueryV2::QueryParameterType.new(
+          type: "STRING"
+        ),
+        parameter_value: Google::Apis::BigqueryV2::QueryParameterValue.new(
+          value: json_value.to_json
+        )
+      )
+
+      actual = Google::Cloud::Bigquery::Convert.to_query_param json_value.to_json
+      assert_equal expected.to_h, actual.to_h
+    end
+
+    it "allows string when using type" do
+      expected = Google::Apis::BigqueryV2::QueryParameter.new(
+        parameter_type: Google::Apis::BigqueryV2::QueryParameterType.new(
+          type: "JSON"
+        ),
+        parameter_value: Google::Apis::BigqueryV2::QueryParameterValue.new(
+          value: json_value.to_json
+        )
+      )
+
+      actual = Google::Cloud::Bigquery::Convert.to_query_param json_value.to_json, :JSON
+      assert_equal expected.to_h, actual.to_h
+    end
+
+    it "allows nil when using type" do
+      expected = Google::Apis::BigqueryV2::QueryParameter.new(
+        parameter_type: Google::Apis::BigqueryV2::QueryParameterType.new(
+          type: "JSON"
+        ),
+        parameter_value: Google::Apis::BigqueryV2::QueryParameterValue.new(
+          value: nil
+        )
+      )
+
+      actual = Google::Cloud::Bigquery::Convert.to_query_param nil, :JSON
+      assert_equal expected.to_h, actual.to_h
+    end
+  end
+
   describe :TIMESTAMP do
     it "identifies by value" do
       expected = Google::Apis::BigqueryV2::QueryParameter.new(
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb
index 1f744881b86a..7de9a9226252 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb
@@ -48,6 +48,7 @@
     _(data[0][:target_end]).must_equal Time.parse("2017-01-01 00:00:00 UTC").to_datetime
     _(data[0][:birthday]).must_equal Date.parse("1968-10-20")
     _(data[0][:home]).must_equal "POINT(-122.335503 47.625536)"
+    _(data[0][:address]).must_equal({ "name" => "Alice", "age" => 30}.to_json)
 
     _(data[1]).must_be_kind_of Hash
     _(data[1][:name]).must_equal "Aaron"
@@ -112,8 +113,13 @@
     _(data.schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(data.schema).must_be :frozen?
     _(data.fields).must_equal data.schema.fields
-    _(data.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(data.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(data.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                :avatar, :started_at, :duration, :target_end, 
+                                :birthday, :home, :address]
+    _(data.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                     my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                     started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                     birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
   end
 
   it "handles missing rows and fields" do
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/external_csv_source_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/external_csv_source_test.rb
index a99b19eb9361..395d9c405425 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/external_csv_source_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/external_csv_source_test.rb
@@ -202,7 +202,8 @@
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "duration",      type: "TIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "target_end",    type: "DATETIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "birthday",      type: "DATE", description: nil, fields: []),
-        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: [])
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: []),
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "address",       type: "JSON", description: nil, fields: [])
       ]),
       csv_options: Google::Apis::BigqueryV2::CsvOptions.new
     )
@@ -223,14 +224,20 @@
       s.datetime "target_end"
       s.date "birthday"
       s.geography "home"
+      s.json "address"
     end
 
     _(table.schema).wont_be :empty?
     _(table.fields).must_equal table.schema.fields
     _(table.headers).must_equal table.schema.headers
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
     _(table.param_types).must_equal table.schema.param_types
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
 
     _(table.to_gapi.to_h).must_equal table_gapi.to_h
   end
@@ -255,7 +262,8 @@
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "duration",      type: "TIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "target_end",    type: "DATETIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "birthday",      type: "DATE", description: nil, fields: []),
-        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: [])
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: []),
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "address",       type: "JSON", description: nil, fields: [])
       ]),
       csv_options: Google::Apis::BigqueryV2::CsvOptions.new
     )
@@ -277,15 +285,20 @@
     schema.datetime "target_end"
     schema.date "birthday"
     schema.geography "home"
+    schema.json "address"
 
     table.schema = schema
 
     _(table.schema).wont_be :empty?
     _(table.fields).must_equal table.schema.fields
     _(table.headers).must_equal table.schema.headers
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
     _(table.param_types).must_equal table.schema.param_types
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, 
+                                      active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, 
+                                      target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
 
     _(table.to_gapi.to_h).must_equal table_gapi.to_h
   end
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/external_json_source_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/external_json_source_test.rb
index c75cdae08216..062458ca9c8b 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/external_json_source_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/external_json_source_test.rb
@@ -59,7 +59,8 @@
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "duration",      type: "TIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "target_end",    type: "DATETIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "birthday",      type: "DATE", description: nil, fields: []),
-        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: [])
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: []),
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "address",       type: "JSON", description: nil, fields: [])
       ])
     )
 
@@ -79,14 +80,20 @@
       s.datetime "target_end"
       s.date "birthday"
       s.geography "home"
+      s.json "address"
     end
 
     _(table.schema).wont_be :empty?
     _(table.fields).must_equal table.schema.fields
     _(table.headers).must_equal table.schema.headers
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
     _(table.param_types).must_equal table.schema.param_types
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
 
     _(table.to_gapi.to_h).must_equal table_gapi.to_h
   end
@@ -111,7 +118,8 @@
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "duration",      type: "TIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "target_end",    type: "DATETIME", description: nil, fields: []),
         Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "birthday",      type: "DATE", description: nil, fields: []),
-        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: [])
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "home",          type: "GEOGRAPHY", description: nil, fields: []),
+        Google::Apis::BigqueryV2::TableFieldSchema.new(mode: "NULLABLE", name: "address",       type: "JSON", description: nil, fields: [])
       ])
     )
 
@@ -132,15 +140,21 @@
     schema.datetime "target_end"
     schema.date "birthday"
     schema.geography "home"
+    schema.json "address"
 
     table.schema = schema
 
     _(table.schema).wont_be :empty?
     _(table.fields).must_equal table.schema.fields
     _(table.headers).must_equal table.schema.headers
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
     _(table.param_types).must_equal table.schema.param_types
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
 
     _(table.to_gapi.to_h).must_equal table_gapi.to_h
   end
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/load_job_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/load_job_test.rb
index b3b072b811fc..3140c18303c9 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/load_job_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/load_job_test.rb
@@ -120,7 +120,9 @@
     _(job.schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(job.schema).must_be :frozen?
     _(job.schema.fields).wont_be :empty?
-    _(job.schema.fields.map(&:name)).must_equal ["name", "age", "score", "pi", "my_bignumeric", "active", "avatar", "started_at", "duration", "target_end", "birthday", "home"]
+    _(job.schema.fields.map(&:name)).must_equal ["name", "age", "score", "pi", "my_bignumeric", "active", 
+                                                 "avatar", "started_at", "duration", "target_end", 
+                                                 "birthday", "home", "address"]
   end
 
   it "knows its load config" do
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/query_job_data_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/query_job_data_test.rb
index 65715324b970..8a5f403990ab 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/query_job_data_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/query_job_data_test.rb
@@ -50,6 +50,7 @@
     _(data[0][:target_end]).must_equal Time.parse("2017-01-01 00:00:00 UTC").to_datetime
     _(data[0][:birthday]).must_equal Date.parse("1968-10-20")
     _(data[0][:home]).must_equal "POINT(-122.335503 47.625536)"
+    _(data[0][:address]).must_equal({ "name" => "Alice", "age" => 30}.to_json)
 
     _(data[1]).must_be_kind_of Hash
     _(data[1][:name]).must_equal "Aaron"
@@ -104,6 +105,7 @@
     _(data[0][:target_end]).must_equal Time.parse("2017-01-01 00:00:00 UTC").to_datetime
     _(data[0][:birthday]).must_equal Date.parse("1968-10-20")
     _(data[0][:home]).must_equal "POINT(-122.335503 47.625536)"
+    _(data[0][:address]).must_equal({ "name" => "Alice", "age" => 30}.to_json)
 
     _(data[1]).must_be_kind_of Hash
     _(data[1][:name]).must_equal "Aaron"
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb
index 2388c6d7ae82..a7b46fe1b162 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb
@@ -106,6 +106,11 @@
           "name" => "home",
           "type" => "GEOGRAPHY",
           "mode" => "NULLABLE"
+        },
+        {
+          "name" => "address",
+          "type" => "JSON",
+          "mode" => "NULLABLE"
         }
       ]
     }
@@ -131,7 +136,9 @@
   it "has basic values" do
     _(schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(schema.fields).wont_be :empty?
-    _(schema.fields.map(&:name)).must_equal ["name", "age", "score", "pi", "my_bignumeric", "active", "avatar", "started_at", "duration", "target_end", "birthday", "alts", "home"]
+    _(schema.fields.map(&:name)).must_equal ["name", "age", "score", "pi", "my_bignumeric", "active", 
+                                             "avatar", "started_at", "duration", "target_end", 
+                                             "birthday", "alts", "home", "address"]
   end
 
   it "can access fields with a symbol" do
@@ -256,6 +263,13 @@
     _(schema.field(:home).mode).must_equal "NULLABLE"
     _(schema.field(:home)).must_be :geography?
     _(schema.field(:home)).must_be :nullable?
+
+    _(schema.field(:address)).must_be_kind_of Google::Cloud::Bigquery::Schema::Field
+    _(schema.field(:address).name).must_equal "address"
+    _(schema.field(:address).type).must_equal "JSON"
+    _(schema.field(:address).mode).must_equal "NULLABLE"
+    _(schema.field(:address)).must_be :json?
+    _(schema.field(:address)).must_be :nullable?
   end
 
   it "can access fields with a string" do
@@ -379,6 +393,13 @@
     _(schema.field("home").mode).must_equal "NULLABLE"
     _(schema.field("home")).must_be :geography?
     _(schema.field("home")).must_be :nullable?
+
+    _(schema.field("address")).must_be_kind_of Google::Cloud::Bigquery::Schema::Field
+    _(schema.field("address").name).must_equal "address"
+    _(schema.field("address").type).must_equal "JSON"
+    _(schema.field("address").mode).must_equal "NULLABLE"
+    _(schema.field("address")).must_be :json?
+    _(schema.field("address")).must_be :nullable?
   end
 
   it "can load the schema from a File" do
@@ -567,7 +588,7 @@
         file.delete
       end
     end
-    _(json.length).must_equal 13
+    _(json.length).must_equal 14
 
     name = json.find { |record| record["name"] == "name" }
     _(name["type"]).must_equal "STRING"
@@ -640,6 +661,10 @@
     home = json.find { |record| record["name"] == "home" }
     _(home["type"]).must_equal "GEOGRAPHY"
     _(home["mode"]).must_equal "NULLABLE"
+
+    address = json.find { |record| record["name"] == "address" }
+    _(address["type"]).must_equal "JSON"
+    _(address["mode"]).must_equal "NULLABLE"
   end
 
   it "can dump the schema as JSON to a filename" do
@@ -655,7 +680,7 @@
         file.delete
       end
     end
-    _(json.length).must_equal 13
+    _(json.length).must_equal 14
 
     name = json.find { |record| record["name"] == "name" }
     _(name["type"]).must_equal "STRING"
@@ -728,6 +753,10 @@
     home = json.find { |record| record["name"] == "home" }
     _(home["type"]).must_equal "GEOGRAPHY"
     _(home["mode"]).must_equal "NULLABLE"
+
+    address = json.find { |record| record["name"] == "address" }
+    _(address["type"]).must_equal "JSON"
+    _(address["mode"]).must_equal "NULLABLE"
   end
 
   it "can load a schema with the class method" do
@@ -751,10 +780,10 @@
         file.delete
       end
     end
-    _(json.length).must_equal 13
+    _(json.length).must_equal 14
 
     fields = json.map { |record| record["name"] }
     _(fields).wont_be :empty?
-    _(fields).must_equal %w[name age score pi my_bignumeric active avatar started_at duration target_end birthday alts home]
+    _(fields).must_equal %w[name age score pi my_bignumeric active avatar started_at duration target_end birthday alts home address]
   end
 end
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb
index 691d55d9f913..6eeb13c72c0a 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb
@@ -95,8 +95,13 @@
     _(table.schema).must_be :frozen?
     _(table.schema.fields).wont_be :empty?
     _(table.fields).wont_be :empty?
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
 
     mock.verify
 
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb
index 142adf2b2e73..92c5d0225864 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb
@@ -57,6 +57,7 @@
   let(:field_datetime_gapi) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "target_end", type: "DATETIME", mode: "NULLABLE", description: nil, fields: [] }
   let(:field_date_gapi) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "birthday", type: "DATE", mode: "NULLABLE", description: nil, fields: [] }
   let(:field_geography_gapi) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "home", type: "GEOGRAPHY", mode: "NULLABLE", description: nil, fields: [] }
+  let(:field_json_gapi) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "address", type: "JSON", mode: "NULLABLE", description: nil, fields: [] }
   let(:field_record_repeated_gapi) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "cities_lived", type: "RECORD", mode: "REPEATED", description: nil, fields: [ field_integer_gapi, field_timestamp_gapi ] }
 
   let(:field_string_required_gapi_default) { Google::Apis::BigqueryV2::TableFieldSchema.new name: "first_name", type: "STRING", mode: "REQUIRED", description: nil, fields: [], max_length: max_length_string, default_value_expression: "'name'" }
@@ -93,7 +94,7 @@
   it "gets the schema, fields, and headers" do
     _(table.schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(table.schema).must_be :frozen?
-    _(table.schema.fields.count).must_equal 12
+    _(table.schema.fields.count).must_equal 13
 
     _(table.schema.fields[0].name).must_equal "name"
     _(table.schema.fields[0].type).must_equal "STRING"
@@ -155,10 +156,20 @@
     _(table.schema.fields[11].description).must_be :nil?
     _(table.schema.fields[11].mode).must_equal "NULLABLE"
 
-    _(table.fields.count).must_equal 12
+    _(table.schema.fields[12].name).must_equal "address"
+    _(table.schema.fields[12].type).must_equal "JSON"
+    _(table.schema.fields[12].description).must_be :nil?
+    _(table.schema.fields[12].mode).must_equal "NULLABLE"
+
+    _(table.fields.count).must_equal 13
     _(table.fields.map(&:name)).must_equal table.schema.fields.map(&:name)
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
   end
 
   it "sets a flat schema via a block with replace option true" do
@@ -174,7 +185,8 @@
                field_time_gapi,
                field_datetime_gapi,
                field_date_gapi,
-               field_geography_gapi])
+               field_geography_gapi,
+               field_json_gapi])
 
     mock = Minitest::Mock.new
     returned_table_gapi = table_gapi.dup
@@ -198,6 +210,7 @@
       schema.datetime "target_end"
       schema.date "birthday"
       schema.geography "home"
+      schema.json "address"
     end
 
     _(table.schema.field("rank").max_length).must_be :nil?
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/table_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/table_test.rb
index 09273b3cd705..0c539023eb24 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/table_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/table_test.rb
@@ -104,8 +104,13 @@
     _(table.schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(table.schema).must_be :frozen?
     _(table.fields.map(&:name)).must_equal table.schema.fields.map(&:name)
-    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(table.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                 :avatar, :started_at, :duration, :target_end, 
+                                 :birthday, :home, :address]
+    _(table.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                      my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                      started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                      birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
   end
 
   it "knows its streaming buffer attributes" do
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb
index 9166325ae0cf..e42e6882d94b 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb
@@ -79,9 +79,13 @@
     _(view.schema).must_be :frozen?
     _(view.schema.fields).wont_be :empty?
     _(view.fields).wont_be :empty?
-    _(view.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(view.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
-
+    _(view.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                :avatar, :started_at, :duration, :target_end, 
+                                :birthday, :home, :address]
+    _(view.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                     my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                     started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                     birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
     mock.verify
 
     # A second call to attribute does not make a second HTTP API call
diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/view_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/view_test.rb
index 6f22284939df..1743a390777c 100644
--- a/google-cloud-bigquery/test/google/cloud/bigquery/view_test.rb
+++ b/google-cloud-bigquery/test/google/cloud/bigquery/view_test.rb
@@ -64,8 +64,13 @@
     _(view.schema).must_be_kind_of Google::Cloud::Bigquery::Schema
     _(view.schema).must_be :frozen?
     _(view.fields.map(&:name)).must_equal view.schema.fields.map(&:name)
-    _(view.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, :avatar, :started_at, :duration, :target_end, :birthday, :home]
-    _(view.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, birthday: :DATE, home: :GEOGRAPHY })
+    _(view.headers).must_equal [:name, :age, :score, :pi, :my_bignumeric, :active, 
+                                :avatar, :started_at, :duration, :target_end, 
+                                :birthday, :home, :address]
+    _(view.param_types).must_equal({ name: :STRING, age: :INTEGER, score: :FLOAT, pi: :NUMERIC, 
+                                     my_bignumeric: :BIGNUMERIC, active: :BOOLEAN, avatar: :BYTES, 
+                                     started_at: :TIMESTAMP, duration: :TIME, target_end: :DATETIME, 
+                                     birthday: :DATE, home: :GEOGRAPHY, address: :JSON })
   end
 
   it "can test its existence" do
diff --git a/google-cloud-bigquery/test/helper.rb b/google-cloud-bigquery/test/helper.rb
index ce2ca1cb5f8b..4060899129d5 100644
--- a/google-cloud-bigquery/test/helper.rb
+++ b/google-cloud-bigquery/test/helper.rb
@@ -187,6 +187,11 @@ def random_schema_hash
           "name" => "home",
           "type" => "GEOGRAPHY",
           "mode" => "NULLABLE"
+        },
+        {
+          "name" => "address",
+          "type" => "JSON",
+          "mode" => "NULLABLE"
         }
       ]
     }
@@ -207,7 +212,8 @@ def random_data_rows
           { "v" => "04:00:00" },
           { "v" => "2017-01-01 00:00:00" },
           { "v" => "1968-10-20" },
-          { "v" => "POINT(-122.335503 47.625536)" }
+          { "v" => "POINT(-122.335503 47.625536)" },
+          { "v" => { "name" => "Alice", "age" => 30 }.to_json }
         ]
       },
       {
@@ -223,6 +229,7 @@ def random_data_rows
           { "v" => "04:32:10.555555" },
           { "v" => nil },
           { "v" => nil },
+          { "v" => nil },
           { "v" => nil }
         ]
       },
@@ -239,6 +246,7 @@ def random_data_rows
           { "v" => nil },
           { "v" => nil },
           { "v" => nil },
+          { "v" => nil },
           { "v" => nil }
         ]
       }