diff --git a/java-bigquery-jdbc/pom.xml b/java-bigquery-jdbc/pom.xml
index 24bd9b89eec9..c3a9f1dadf24 100644
--- a/java-bigquery-jdbc/pom.xml
+++ b/java-bigquery-jdbc/pom.xml
@@ -456,6 +456,20 @@
2.96.0-SNAPSHOT
test
+
+ com.google.cloud
+ google-cloud-bigquerystorage
+ 3.32.0-SNAPSHOT
+ tests
+ test-jar
+ test
+
+
+ com.google.api
+ gax-grpc
+ testlib
+ test
+
diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
index 61faefb3fc5c..62a8992af2d6 100644
--- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
+++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java
@@ -34,6 +34,7 @@
import com.google.cloud.bigquery.storage.v1.TableName;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.protobuf.Descriptors.DescriptorValidationException;
import java.io.IOException;
@@ -392,7 +393,9 @@ private long bulkInsertWithWriteAPI(BigQueryWriteClient bigQueryWriteClient)
JsonObject rowObject = new JsonObject();
for (int j = 0; j < parameterList.size(); j++) {
BigQueryJdbcParameter parameter = parameterList.get(j);
- if (parameter.getSqlType() == StandardSQLTypeName.STRING) {
+ if (parameter.getValue() == null) {
+ rowObject.add(fieldLists.get(j).getName(), JsonNull.INSTANCE);
+ } else if (parameter.getSqlType() == StandardSQLTypeName.STRING) {
rowObject.addProperty(fieldLists.get(j).getName(), parameter.getValue().toString());
} else {
rowObject.addProperty(fieldLists.get(j).getName(), gson.toJson(parameter.getValue()));
diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementBulkInsertNullValueTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementBulkInsertNullValueTest.java
new file mode 100644
index 000000000000..5ccdde1ed80b
--- /dev/null
+++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatementBulkInsertNullValueTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2026 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigquery.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.Mockito.mock;
+
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.grpc.testing.LocalChannelProvider;
+import com.google.api.gax.grpc.testing.MockGrpcService;
+import com.google.api.gax.grpc.testing.MockServiceHelper;
+import com.google.cloud.bigquery.Field;
+import com.google.cloud.bigquery.Schema;
+import com.google.cloud.bigquery.StandardSQLTypeName;
+import com.google.cloud.bigquery.storage.v1.AppendRowsResponse;
+import com.google.cloud.bigquery.storage.v1.BatchCommitWriteStreamsResponse;
+import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient;
+import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings;
+import com.google.cloud.bigquery.storage.v1.FinalizeWriteStreamResponse;
+import com.google.cloud.bigquery.storage.v1.MockBigQueryWrite;
+import com.google.cloud.bigquery.storage.v1.TableFieldSchema;
+import com.google.cloud.bigquery.storage.v1.TableName;
+import com.google.cloud.bigquery.storage.v1.TableSchema;
+import com.google.cloud.bigquery.storage.v1.WriteStream;
+import com.google.protobuf.Int64Value;
+import com.google.protobuf.Timestamp;
+import java.lang.reflect.Method;
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.UUID;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for https://github.com/googleapis/google-cloud-java/issues/14066: a
+ * null-valued STRING parameter in a Storage Write API batch insert used to throw NPE while
+ * building the row's JSON representation, instead of writing a JSON null. Drives the real
+ * bulkInsertWithWriteAPI code path end-to-end against an in-process fake Storage Write service so
+ * the fix is verified without any real network dependency.
+ */
+public class BigQueryPreparedStatementBulkInsertNullValueTest {
+
+ private static final String STREAM_NAME =
+ "projects/test-project/datasets/test_dataset/tables/test_table/streams/test-stream";
+
+ private MockBigQueryWrite mockBigQueryWrite;
+ private MockServiceHelper serviceHelper;
+ private BigQueryWriteClient writeClient;
+ private BigQueryPreparedStatement preparedStatement;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ mockBigQueryWrite = new MockBigQueryWrite();
+ serviceHelper =
+ new MockServiceHelper(
+ UUID.randomUUID().toString(), Arrays.asList(mockBigQueryWrite));
+ serviceHelper.start();
+
+ LocalChannelProvider channelProvider = serviceHelper.createChannelProvider();
+ writeClient =
+ BigQueryWriteClient.create(
+ BigQueryWriteSettings.newBuilder()
+ .setTransportChannelProvider(channelProvider)
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .build());
+
+ TableFieldSchema nameField =
+ TableFieldSchema.newBuilder()
+ .setName("name")
+ .setType(TableFieldSchema.Type.STRING)
+ .setMode(TableFieldSchema.Mode.NULLABLE)
+ .build();
+ WriteStream writeStream =
+ WriteStream.newBuilder()
+ .setName(STREAM_NAME)
+ .setTableSchema(TableSchema.newBuilder().addFields(nameField).build())
+ .build();
+
+ // Responses are consumed in call order by bulkInsertWithWriteAPI: createWriteStream, then
+ // append, then finalizeWriteStream, then batchCommitWriteStreams.
+ mockBigQueryWrite.addResponse(writeStream);
+ mockBigQueryWrite.addResponse(
+ AppendRowsResponse.newBuilder()
+ .setAppendResult(
+ AppendRowsResponse.AppendResult.newBuilder().setOffset(Int64Value.of(0)))
+ .build());
+ mockBigQueryWrite.addResponse(FinalizeWriteStreamResponse.newBuilder().setRowCount(1).build());
+ mockBigQueryWrite.addResponse(
+ BatchCommitWriteStreamsResponse.newBuilder()
+ .setCommitTime(Timestamp.newBuilder().build())
+ .build());
+
+ BigQueryConnection connection = mock(BigQueryConnection.class);
+ preparedStatement =
+ new BigQueryPreparedStatement(connection, "INSERT INTO test_table (name) VALUES (?)");
+ preparedStatement.insertSchema = Schema.of(Field.of("name", StandardSQLTypeName.STRING));
+
+ java.lang.reflect.Field insertTableNameField =
+ BigQueryPreparedStatement.class.getDeclaredField("insertTableName");
+ insertTableNameField.setAccessible(true);
+ insertTableNameField.set(
+ preparedStatement, TableName.of("test-project", "test_dataset", "test_table"));
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ writeClient.close();
+ serviceHelper.stop();
+ }
+
+ @Test
+ public void testBulkInsertWithWriteApiCompletesForNullStringParameter() throws Exception {
+ preparedStatement.setNull(1, Types.VARCHAR);
+ preparedStatement.addBatch();
+
+ Method bulkInsertMethod =
+ BigQueryPreparedStatement.class.getDeclaredMethod(
+ "bulkInsertWithWriteAPI", BigQueryWriteClient.class);
+ bulkInsertMethod.setAccessible(true);
+
+ assertDoesNotThrow(() -> bulkInsertMethod.invoke(preparedStatement, writeClient));
+ }
+}