From ea3bdbfcce26743f8869080284f7e1c7c20d30f0 Mon Sep 17 00:00:00 2001 From: Lawrence Qiu Date: Fri, 31 Jul 2026 19:19:29 +0000 Subject: [PATCH] fix(storage): use JsonUtils for StorageObject serialization in resumable writes and read channels When upgrading gRPC to v1.83.0 (and Gson to 2.14.0), Gson PR #3006 added strict duplicate key detection in MapTypeAdapterFactory.Adapter.read ('if (map.containsKey(key)) throw ...'). Because com.google.api.services.storage.model.StorageObject extends com.google.api.client.util.GenericData (which implements java.util.Map), raw Gson selects MapTypeAdapterFactory when serializing and deserializing StorageObject. However, GenericData.containsKey returns true for all @Key annotated fields even before they are set, causing raw Gson.fromJson to throw JsonSyntaxException ('duplicate key') when deserializing StorageObject. This replaces raw Gson serialization and deserialization of StorageObject in ApiaryReadRequest and JsonResumableWrite with Google API Client's official JSON parser/serializer via JsonUtils (backed by JsonObjectParser / GsonFactory), resolving the duplicate key deserialization failure in SerializationTest. --- .../ApiaryUnbufferedReadableByteChannel.java | 9 ++------ .../cloud/storage/JsonResumableWrite.java | 10 ++------- .../com/google/cloud/storage/JsonUtils.java | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java index 781c449369cb..25757921ba54 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryUnbufferedReadableByteChannel.java @@ -41,14 +41,11 @@ import com.google.common.hash.HashingInputStream; import com.google.common.io.BaseEncoding; import com.google.common.primitives.Ints; -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.io.StringReader; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.channels.Channels; @@ -381,7 +378,6 @@ private static String getHeaderValue(@NonNull HttpHeaders headers, @NonNull Stri @Immutable static final class ApiaryReadRequest implements Serializable { private static final long serialVersionUID = -4059435314115374448L; - private static final Gson gson = new Gson(); @NonNull private transient StorageObject object; @NonNull private final Map options; @NonNull private final ByteRangeSpec byteRangeSpec; @@ -450,7 +446,7 @@ private String getObjectJson() { if (objectJson == null) { synchronized (this) { if (objectJson == null) { - objectJson = gson.toJson(object); + objectJson = JsonUtils.objectToJson(object); } } } @@ -464,8 +460,7 @@ private void writeObject(ObjectOutputStream out) throws IOException { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - JsonReader jsonReader = gson.newJsonReader(new StringReader(this.objectJson)); - this.object = gson.fromJson(jsonReader, StorageObject.class); + this.object = JsonUtils.jsonToObject(this.objectJson, StorageObject.class); } } } diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java index b2347c47f3cf..3fef2dcbb9dc 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java @@ -22,13 +22,10 @@ import com.google.cloud.storage.spi.v1.StorageRpc; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; -import com.google.gson.stream.JsonReader; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.io.StringReader; import java.util.Map; import java.util.Objects; import org.checkerframework.checker.lock.qual.GuardedBy; @@ -37,8 +34,6 @@ final class JsonResumableWrite implements Serializable { private static final long serialVersionUID = 7934407897802252292L; - private static final Gson gson = new Gson(); - @MonotonicNonNull private transient StorageObject object; @MonotonicNonNull private transient Hasher hasher; @MonotonicNonNull private transient Crc32cValue cumulativeCrc32c; @@ -152,7 +147,7 @@ private String getObjectJson() { if (objectJson == null) { synchronized (this) { if (objectJson == null) { - objectJson = gson.toJson(object); + objectJson = JsonUtils.objectToJson(object); base64CumulativeCrc32c = Utils.crc32cCodec.encode(cumulativeCrc32c.getValue()); } } @@ -167,8 +162,7 @@ private void writeObject(ObjectOutputStream out) throws IOException { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - JsonReader jsonReader = gson.newJsonReader(new StringReader(this.objectJson)); - this.object = gson.fromJson(jsonReader, StorageObject.class); + this.object = JsonUtils.jsonToObject(this.objectJson, StorageObject.class); if (base64CumulativeCrc32c != null) { Integer decode = Utils.crc32cCodec.decode(base64CumulativeCrc32c); if (decode == 0) { diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonUtils.java index f5297ee837c0..6bc8d27d6e44 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonUtils.java @@ -53,6 +53,28 @@ final class JsonUtils { private JsonUtils() {} + static String objectToJson(T object) { + if (object == null) { + return null; + } + try { + return jop.getJsonFactory().toString(object); + } catch (IOException e) { + throw StorageException.coalesce(e); + } + } + + static T jsonToObject(String json, Class clazz) { + if (json == null) { + return null; + } + try { + return jop.parseAndClose(new StringReader(json), clazz); + } catch (IOException e) { + throw StorageException.coalesce(e); + } + } + /** * Given a GenericJson src, and a list of {@code fieldsForOutput} create a new GenericJson where * every field specified in {@code fieldsForOutput} is present. If a field exists in {@code src}