Repository: googleapis/python-bigquery

Issue:
Support JSON data type in `insert_rows` and as query parameters
We need to make sure the JSON data type is supported for `insert_rows`. I suspect it is broken for query parameters, too.

See: https://github.com/googleapis/python-bigquery/pull/1587#issuecomment-1855617976


Discussion:
I think we need to update:

https://github.com/googleapis/python-bigquery/blob/fdd580f2b603ca29f1ef52d094ebb8016d9bdc9d/google/cloud/bigquery/_helpers.py#L442

It should be `json_to_json` and `dumps` rather than `loads`.
Also, update our `insert_rows` and `query_parameters` system tests to use the JSON data type in some column.

Resolving patch:
diff --git a/google/cloud/bigquery/_helpers.py b/google/cloud/bigquery/_helpers.py
--- a/google/cloud/bigquery/_helpers.py
+++ b/google/cloud/bigquery/_helpers.py
@@ -374,6 +374,13 @@ def _bytes_to_json(value):
     return value
 
 
+def _json_to_json(value):
+    """Coerce 'value' to a BigQuery REST API representation."""
+    if value is None:
+        return None
+    return json.dumps(value)
+
+
 def _timestamp_to_json_parameter(value):
     """Coerce 'value' to an JSON-compatible representation.
 
@@ -439,7 +446,7 @@ def _time_to_json(value):
     "DATETIME": _datetime_to_json,
     "DATE": _date_to_json,
     "TIME": _time_to_json,
-    "JSON": _json_from_json,
+    "JSON": _json_to_json,
     # Make sure DECIMAL and BIGDECIMAL are handled, even though
     # requests for them should be converted to NUMERIC.  Better safe
     # than sorry.


Test patch:
diff --git a/tests/system/test_client.py b/tests/system/test_client.py
--- a/tests/system/test_client.py
+++ b/tests/system/test_client.py
@@ -2049,13 +2049,18 @@ def test_insert_rows_nested_nested(self):
                     ),
                 ],
             ),
+            SF("json_col", "JSON"),
         ]
         record = {
             "nested_string": "another string value",
             "nested_repeated": [0, 1, 2],
             "nested_record": {"nested_nested_string": "some deep insight"},
         }
-        to_insert = [("Some value", record)]
+        json_record = {
+            "json_array": [1, 2, 3],
+            "json_object": {"alpha": "abc", "num": 123},
+        }
+        to_insert = [("Some value", record, json_record)]
         table_id = "test_table"
         dataset = self.temp_dataset(_make_dataset_id("issue_2951"))
         table_arg = Table(dataset.table(table_id), schema=schema)
diff --git a/tests/system/test_query.py b/tests/system/test_query.py
--- a/tests/system/test_query.py
+++ b/tests/system/test_query.py
@@ -256,6 +256,18 @@ def test_query_statistics(bigquery_client, query_api_method):
                 )
             ],
         ),
+        pytest.param(
+            "SELECT @json",
+            {"alpha": "abc", "num": [1, 2, 3]},
+            [
+                ScalarQueryParameter(
+                    name="json",
+                    type_="JSON",
+                    value={"alpha": "abc", "num": [1, 2, 3]},
+                )
+            ],
+            id="scalar-json",
+        ),
         (
             "SELECT @naive_time",
             datetime.time(12, 41, 9, 62500),
diff --git a/tests/unit/test__helpers.py b/tests/unit/test__helpers.py
--- a/tests/unit/test__helpers.py
+++ b/tests/unit/test__helpers.py
@@ -886,6 +886,16 @@ def test_w_known_field_type(self):
         converted = self._call_fut(field, original)
         self.assertEqual(converted, str(original))
 
+    def test_w_scalar_none(self):
+        import google.cloud.bigquery._helpers as module_under_test
+
+        scalar_types = module_under_test._SCALAR_VALUE_TO_JSON_ROW.keys()
+        for type_ in scalar_types:
+            field = _make_field(type_)
+            original = None
+            converted = self._call_fut(field, original)
+            self.assertIsNone(converted, msg=f"{type_} did not return None")
+
 
 class Test_single_field_to_json(unittest.TestCase):
     def _call_fut(self, field, value):
@@ -921,6 +931,12 @@ def test_w_scalar_ignores_mode(self):
         converted = self._call_fut(field, original)
         self.assertEqual(converted, original)
 
+    def test_w_scalar_json(self):
+        field = _make_field("JSON")
+        original = {"alpha": "abc", "num": [1, 2, 3]}
+        converted = self._call_fut(field, original)
+        self.assertEqual(converted, json.dumps(original))
+
 
 class Test_repeated_field_to_json(unittest.TestCase):
     def _call_fut(self, field, value):