Repository: googleapis/java-storage
Pull request #2446 (2024-03-14 11:33:43)

deps: update dependency com.google.apis:google-api-services-storage to v1-rev20240311-2.0.0
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.apis:google-api-services-storage](http://nexus.sonatype.org/oss-repository-hosting.html) ([source](http://svn.sonatype.org/spice/tags/oss-parent-7)) | `v1-rev20240307-2.0.0` -> `v1-rev20240311-2.0.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.apis:google-api-services-storage/v1-rev20240311-2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.apis:google-api-services-storage/v1-rev20240311-2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.apis:google-api-services-storage/v1-rev20240307-2.0.0/v1-rev20240311-2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.apis:google-api-services-storage/v1-rev20240307-2.0.0/v1-rev20240311-2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/java-storage).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMzguMSIsInVwZGF0ZWRJblZlciI6IjM3LjIzOC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->



Patch:
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
index e0d657e5..b3873194 100644
--- a/.kokoro/presubmit/graalvm-native-17.cfg
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.27.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.28.1"
 }
 
 env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index 78e4a38c..46b04f3c 100644
--- a/.kokoro/presubmit/graalvm-native.cfg
+++ b/.kokoro/presubmit/graalvm-native.cfg
@@ -3,7 +3,7 @@
 # Configure the docker image for kokoro-trampoline.
 env_vars: {
   key: "TRAMPOLINE_IMAGE"
-  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.27.0"
+  value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.28.1"
 }
 
 env_vars: {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bac6556a..e2eb26d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
 # Changelog
 
+## [2.36.0](https://github.com/googleapis/java-storage/compare/v2.35.0...v2.36.0) (2024-03-15)
+
+
+### Features
+
+* Add Custom Part Metadata Decorator to ParallelCompositeUploadConfig ([#2434](https://github.com/googleapis/java-storage/issues/2434)) ([43b8006](https://github.com/googleapis/java-storage/commit/43b800645ba3622e5de635825e1d082c6d26c2eb))
+* Add hierarchical namespace and folders features ([#2445](https://github.com/googleapis/java-storage/issues/2445)) ([8074fff](https://github.com/googleapis/java-storage/commit/8074fffed5208a8578e5afe694fdd3d8df627b8c))
+* Add soft delete feature ([#2403](https://github.com/googleapis/java-storage/issues/2403)) ([989f36f](https://github.com/googleapis/java-storage/commit/989f36fbb206832a6a3584c77546d3d560ac0df8))
+
+
+### Bug Fixes
+
+* Fix name digest for noprefix ([#2448](https://github.com/googleapis/java-storage/issues/2448)) ([12c9db8](https://github.com/googleapis/java-storage/commit/12c9db8935f25a5f9e4633af85ea96acaa914d23))
+* Missing serialVersionUID of serializable classes ([#2344](https://github.com/googleapis/java-storage/issues/2344)) ([736865b](https://github.com/googleapis/java-storage/commit/736865b4b97aa3940e1eab6a582d0ef38db31bba))
+
+
+### Dependencies
+
+* Update dependency com.google.apis:google-api-services-storage to v1-rev20240307-2.0.0 ([#2442](https://github.com/googleapis/java-storage/issues/2442)) ([1352203](https://github.com/googleapis/java-storage/commit/1352203859c3798423ef78823ed10577b93eebef))
+* Update dependency net.jqwik:jqwik to v1.8.4 ([#2447](https://github.com/googleapis/java-storage/issues/2447)) ([110b80c](https://github.com/googleapis/java-storage/commit/110b80cdde24da4868e46c3909020db7b02d5491))
+* Update gcr.io/cloud-devrel-public-resources/storage-testbench docker tag to v0.42.0 ([#2441](https://github.com/googleapis/java-storage/issues/2441)) ([80745d4](https://github.com/googleapis/java-storage/commit/80745d415810353b3466a3eda12f6ca50a2c71b0))
+
 ## [2.35.0](https://github.com/googleapis/java-storage/compare/v2.34.0...v2.35.0) (2024-03-04)
 
 
diff --git a/README.md b/README.md
index 90030c80..9e0ad82f 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.35.0</version>
+  <version>2.36.0</version>
 </dependency>
 
 ```
@@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-storage'
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.35.0'
+implementation 'com.google.cloud:google-cloud-storage:2.36.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.35.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.36.0"
 ```
 <!-- {x-version-update-end} -->
 
@@ -430,7 +430,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.35.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.36.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index d02b72dd..a45465c1 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml
index bc031120..7a073928 100644
--- a/google-cloud-storage-bom/pom.xml
+++ b/google-cloud-storage-bom/pom.xml
@@ -19,12 +19,12 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-bom</artifactId>
-  <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>pom</packaging>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>sdk-platform-java-config</artifactId>
-    <version>3.27.0</version>
+    <version>3.28.1</version>
     <relativePath/>
   </parent>
 
@@ -69,37 +69,37 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-control</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/google-cloud-storage-control/pom.xml b/google-cloud-storage-control/pom.xml
index 747b2b3d..73e6fa93 100644
--- a/google-cloud-storage-control/pom.xml
+++ b/google-cloud-storage-control/pom.xml
@@ -5,13 +5,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-control</artifactId>
-    <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+    <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
     <name>google-cloud-storage-control</name>
     <description>GRPC library for google-cloud-storage-control</description>
     <parent>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-parent</artifactId>
-        <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </parent>
 
     <dependencies>
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index ba323387..7fffdb6b 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -15,12 +15,36 @@
     <method>* writeAndClose(*)</method>
   </difference>
 
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/storage/BucketInfo$Builder</className>
+    <method>com.google.cloud.storage.BucketInfo$Builder setHierarchicalNamespace(com.google.cloud.storage.BucketInfo$HierarchicalNamespace)</method>
+  </difference>
+
   <difference>
     <differenceType>7013</differenceType>
     <className>com/google/cloud/storage/BlobInfo$Builder</className>
     <method>com.google.cloud.storage.BlobInfo$Builder setRetention(com.google.cloud.storage.BlobInfo$Retention)</method>
   </difference>
 
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/storage/BucketInfo$Builder</className>
+    <method>com.google.cloud.storage.BucketInfo$Builder setSoftDeletePolicy(com.google.cloud.storage.BucketInfo$SoftDeletePolicy)</method>
+  </difference>
+
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/storage/Storage</className>
+    <method>com.google.cloud.storage.Blob restore(com.google.cloud.storage.BlobId, com.google.cloud.storage.Storage$BlobRestoreOption[])</method>
+  </difference>
+
+  <difference>
+    <differenceType>7012</differenceType>
+    <className>com/google/cloud/storage/spi/v1/StorageRpc</className>
+    <method>com.google.api.services.storage.model.StorageObject restore(com.google.api.services.storage.model.StorageObject, java.util.Map)</method>
+  </difference>
+
   <!-- @BetaApi members -->
   <difference>
     <differenceType>7009</differenceType>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 604b126f..e6514ed6 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
@@ -248,7 +248,7 @@
     <dependency>
       <groupId>net.jqwik</groupId>
       <artifactId>jqwik</artifactId>
-      <version>1.8.3</version>
+      <version>1.8.4</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
index e6295b8c..c6ea7a2c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
@@ -526,6 +526,18 @@ public class Blob extends BlobInfo {
       return this;
     }
 
+    @Override
+    Builder setSoftDeleteTime(OffsetDateTime softDeleteTime) {
+      infoBuilder.setSoftDeleteTime(softDeleteTime);
+      return this;
+    }
+
+    @Override
+    Builder setHardDeleteTime(OffsetDateTime hardDeleteTime) {
+      infoBuilder.setHardDeleteTime(hardDeleteTime);
+      return this;
+    }
+
     @Override
     public Builder setRetention(Retention retention) {
       infoBuilder.setRetention(retention);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
index e808f444..52ce09fd 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
@@ -108,6 +108,8 @@ public class BlobInfo implements Serializable {
   private final Boolean temporaryHold;
   private final OffsetDateTime retentionExpirationTime;
   private final Retention retention;
+  private final OffsetDateTime softDeleteTime;
+  private final OffsetDateTime hardDeleteTime;
   private final transient ImmutableSet<NamedField> modifiedFields;
 
   /** This class is meant for internal use only. Users are discouraged from using this class. */
@@ -525,6 +527,10 @@ public class BlobInfo implements Serializable {
       return setRetentionExpirationTime(millisOffsetDateTimeCodec.decode(retentionExpirationTime));
     }
 
+    abstract Builder setSoftDeleteTime(OffsetDateTime offsetDateTime);
+
+    abstract Builder setHardDeleteTime(OffsetDateTime hardDeleteTIme);
+
     public abstract Builder setRetention(Retention retention);
 
     /** Creates a {@code BlobInfo} object. */
@@ -626,6 +632,8 @@ public class BlobInfo implements Serializable {
     private Boolean temporaryHold;
     private OffsetDateTime retentionExpirationTime;
     private Retention retention;
+    private OffsetDateTime softDeleteTime;
+    private OffsetDateTime hardDeleteTime;
     private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
 
     BuilderImpl(BlobId blobId) {
@@ -664,6 +672,8 @@ public class BlobInfo implements Serializable {
       temporaryHold = blobInfo.temporaryHold;
       retentionExpirationTime = blobInfo.retentionExpirationTime;
       retention = blobInfo.retention;
+      softDeleteTime = blobInfo.softDeleteTime;
+      hardDeleteTime = blobInfo.hardDeleteTime;
     }
 
     @Override
@@ -1037,6 +1047,24 @@ public class BlobInfo implements Serializable {
       return this;
     }
 
+    @Override
+    Builder setSoftDeleteTime(OffsetDateTime softDeleteTime) {
+      if (!Objects.equals(this.softDeleteTime, softDeleteTime)) {
+        modifiedFields.add(BlobField.SOFT_DELETE_TIME);
+      }
+      this.softDeleteTime = softDeleteTime;
+      return this;
+    }
+
+    @Override
+    Builder setHardDeleteTime(OffsetDateTime hardDeleteTime) {
+      if (!Objects.equals(this.hardDeleteTime, hardDeleteTime)) {
+        modifiedFields.add(BlobField.HARD_DELETE_TIME);
+      }
+      this.hardDeleteTime = hardDeleteTime;
+      return this;
+    }
+
     @Override
     public Builder setRetention(Retention retention) {
       // todo: b/308194853
@@ -1269,6 +1297,8 @@ public class BlobInfo implements Serializable {
     temporaryHold = builder.temporaryHold;
     retentionExpirationTime = builder.retentionExpirationTime;
     retention = builder.retention;
+    softDeleteTime = builder.softDeleteTime;
+    hardDeleteTime = builder.hardDeleteTime;
     modifiedFields = builder.modifiedFields.build();
   }
 
@@ -1662,6 +1692,18 @@ public class BlobInfo implements Serializable {
     return retentionExpirationTime;
   }
 
+  /** If this object has been soft-deleted, returns the time it was soft-deleted. */
+  public OffsetDateTime getSoftDeleteTime() {
+    return softDeleteTime;
+  }
+
+  /**
+   * If this object has been soft-deleted, returns the time at which it will be permanently deleted.
+   */
+  public OffsetDateTime getHardDeleteTime() {
+    return hardDeleteTime;
+  }
+
   /** Returns the object's Retention policy. */
   public Retention getRetention() {
     return retention;
@@ -1717,7 +1759,9 @@ public class BlobInfo implements Serializable {
         eventBasedHold,
         temporaryHold,
         retention,
-        retentionExpirationTime);
+        retentionExpirationTime,
+        softDeleteTime,
+        hardDeleteTime);
   }
 
   @Override
@@ -1759,7 +1803,9 @@ public class BlobInfo implements Serializable {
         && Objects.equals(eventBasedHold, blobInfo.eventBasedHold)
         && Objects.equals(temporaryHold, blobInfo.temporaryHold)
         && Objects.equals(retentionExpirationTime, blobInfo.retentionExpirationTime)
-        && Objects.equals(retention, blobInfo.retention);
+        && Objects.equals(retention, blobInfo.retention)
+        && Objects.equals(softDeleteTime, blobInfo.softDeleteTime)
+        && Objects.equals(hardDeleteTime, blobInfo.hardDeleteTime);
   }
 
   ImmutableSet<NamedField> getModifiedFields() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
index 3c1652bc..0587bb0b 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
@@ -748,6 +748,18 @@ public class Bucket extends BucketInfo {
       return this;
     }
 
+    @Override
+    public Builder setSoftDeletePolicy(SoftDeletePolicy softDeletePolicy) {
+      infoBuilder.setSoftDeletePolicy(softDeletePolicy);
+      return this;
+    }
+
+    @Override
+    public Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace) {
+      infoBuilder.setHierarchicalNamespace(hierarchicalNamespace);
+      return this;
+    }
+
     @Override
     public Bucket build() {
       return new Bucket(storage, infoBuilder);
@@ -1083,6 +1095,28 @@ public class Bucket extends BucketInfo {
     return storage.get(BlobId.of(getName(), blob), options);
   }
 
+  /**
+   * Returns the requested blob in this bucket of a specific generation or {@code null} if not
+   * found.
+   *
+   * <p>Example of getting a blob of a specific in the bucket.
+   *
+   * <pre>{@code
+   * String blobName = "my_blob_name";
+   * long generation = 42;
+   * Blob blob = bucket.get(blobName, generation);
+   * }</pre>
+   *
+   * @param blob name of the requested blob
+   * @param generation the generation to get
+   * @param options blob search options
+   * @throws StorageException upon failure
+   */
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+  public Blob get(String blob, Long generation, BlobGetOption... options) {
+    return storage.get(BlobId.of(getName(), blob, generation), options);
+  }
+
   /**
    * Returns a list of requested blobs in this bucket. Blobs that do not exist are null.
    *
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
index 9fb46b5b..32355592 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
@@ -118,6 +118,9 @@ public class BucketInfo implements Serializable {
   private final Logging logging;
   private final CustomPlacementConfig customPlacementConfig;
   private final ObjectRetention objectRetention;
+  private final HierarchicalNamespace hierarchicalNamespace;
+
+  private final SoftDeletePolicy softDeletePolicy;
 
   private final transient ImmutableSet<NamedField> modifiedFields;
 
@@ -349,6 +352,90 @@ public class BucketInfo implements Serializable {
     }
   }
 
+  /**
+   * The bucket's soft delete policy. If this policy is set, any deleted objects will be
+   * soft-deleted according to the time specified in the policy
+   */
+  public static class SoftDeletePolicy implements Serializable {
+
+    private static final long serialVersionUID = -8100190443052242908L;
+    private Duration retentionDuration;
+    private OffsetDateTime effectiveTime;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof SoftDeletePolicy)) {
+        return false;
+      }
+      SoftDeletePolicy that = (SoftDeletePolicy) o;
+      return Objects.equals(retentionDuration, that.retentionDuration)
+          && Objects.equals(effectiveTime, that.effectiveTime);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(retentionDuration, effectiveTime);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("retentionDuration", retentionDuration)
+          .add("effectiveTime", effectiveTime)
+          .toString();
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public Builder toBuilder() {
+      return new Builder().setRetentionDuration(retentionDuration).setEffectiveTime(effectiveTime);
+    }
+
+    private SoftDeletePolicy() {}
+
+    public SoftDeletePolicy(Builder builder) {
+      this.retentionDuration = builder.retentionDuration;
+      this.effectiveTime = builder.effectiveTime;
+    }
+
+    public Duration getRetentionDuration() {
+      return retentionDuration;
+    }
+
+    public OffsetDateTime getEffectiveTime() {
+      return effectiveTime;
+    }
+
+    public static final class Builder {
+      private Duration retentionDuration;
+      private OffsetDateTime effectiveTime;
+
+      /** Sets the length of time to retain soft-deleted objects for, expressed as a Duration */
+      public Builder setRetentionDuration(Duration retentionDuration) {
+        this.retentionDuration = retentionDuration;
+        return this;
+      }
+
+      /**
+       * Sets the time from which this soft-delete policy is effective. This is package-private
+       * because it can only be set by the backend.
+       */
+      Builder setEffectiveTime(OffsetDateTime effectiveTime) {
+        this.effectiveTime = effectiveTime;
+        return this;
+      }
+
+      public SoftDeletePolicy build() {
+        return new SoftDeletePolicy(this);
+      }
+    }
+  }
+
   /**
    * Configuration for the Autoclass settings of a bucket.
    *
@@ -713,6 +800,71 @@ public class BucketInfo implements Serializable {
     }
   }
 
+  /** The bucket's hierarchical namespace (Folders) configuration. Enable this to use HNS. */
+  public static final class HierarchicalNamespace implements Serializable {
+
+    private static final long serialVersionUID = 5932926691444613101L;
+    private Boolean enabled;
+
+    public Boolean getEnabled() {
+      return enabled;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof HierarchicalNamespace)) {
+        return false;
+      }
+      HierarchicalNamespace that = (HierarchicalNamespace) o;
+      return Objects.equals(enabled, that.enabled);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(enabled);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("enabled", enabled).toString();
+    }
+
+    private HierarchicalNamespace() {}
+
+    private HierarchicalNamespace(Builder builder) {
+      this.enabled = builder.enabled;
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public Builder toBuilder() {
+      return newBuilder().setEnabled(enabled);
+    }
+
+    public static final class Builder {
+      private Boolean enabled;
+
+      /**
+       * Sets whether Hierarchical Namespace (Folders) is enabled for this bucket. This can only be
+       * enabled at bucket create time. If this is enabled, Uniform Bucket-Level Access must also be
+       * enabled.
+       */
+      public Builder setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+        return this;
+      }
+
+      public HierarchicalNamespace build() {
+        return new HierarchicalNamespace(this);
+      }
+    }
+  }
+
   /**
    * Lifecycle rule for a bucket. Allows supported Actions, such as deleting and changing storage
    * class, to be executed when certain Conditions are met.
@@ -1683,8 +1835,12 @@ public class BucketInfo implements Serializable {
 
     public abstract Builder setCustomPlacementConfig(CustomPlacementConfig customPlacementConfig);
 
+    public abstract Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace);
+
     abstract Builder setObjectRetention(ObjectRetention objectRetention);
 
+    public abstract Builder setSoftDeletePolicy(SoftDeletePolicy softDeletePolicy);
+
     /** Creates a {@code BucketInfo} object. */
     public abstract BucketInfo build();
 
@@ -1783,6 +1939,9 @@ public class BucketInfo implements Serializable {
     private Logging logging;
     private CustomPlacementConfig customPlacementConfig;
     private ObjectRetention objectRetention;
+
+    private SoftDeletePolicy softDeletePolicy;
+    private HierarchicalNamespace hierarchicalNamespace;
     private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
 
     BuilderImpl(String name) {
@@ -1822,6 +1981,8 @@ public class BucketInfo implements Serializable {
       logging = bucketInfo.logging;
       customPlacementConfig = bucketInfo.customPlacementConfig;
       objectRetention = bucketInfo.objectRetention;
+      softDeletePolicy = bucketInfo.softDeletePolicy;
+      hierarchicalNamespace = bucketInfo.hierarchicalNamespace;
     }
 
     @Override
@@ -2187,6 +2348,24 @@ public class BucketInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setSoftDeletePolicy(SoftDeletePolicy softDeletePolicy) {
+      if (!Objects.equals(this.softDeletePolicy, softDeletePolicy)) {
+        modifiedFields.add(BucketField.SOFT_DELETE_POLICY);
+      }
+      this.softDeletePolicy = softDeletePolicy;
+      return this;
+    }
+
+    @Override
+    public Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace) {
+      if (!Objects.equals(this.hierarchicalNamespace, hierarchicalNamespace)) {
+        modifiedFields.add(BucketField.HIERARCHICAL_NAMESPACE);
+      }
+      this.hierarchicalNamespace = hierarchicalNamespace;
+      return this;
+    }
+
     @Override
     Builder setLocationType(String locationType) {
       if (!Objects.equals(this.locationType, locationType)) {
@@ -2428,6 +2607,8 @@ public class BucketInfo implements Serializable {
     logging = builder.logging;
     customPlacementConfig = builder.customPlacementConfig;
     objectRetention = builder.objectRetention;
+    softDeletePolicy = builder.softDeletePolicy;
+    hierarchicalNamespace = builder.hierarchicalNamespace;
     modifiedFields = builder.modifiedFields.build();
   }
 
@@ -2768,6 +2949,16 @@ public class BucketInfo implements Serializable {
     return objectRetention;
   }
 
+  /** returns the Soft Delete policy */
+  public SoftDeletePolicy getSoftDeletePolicy() {
+    return softDeletePolicy;
+  }
+
+  /** Returns the Hierarchical Namespace (Folders) Configuration */
+  public HierarchicalNamespace getHierarchicalNamespace() {
+    return hierarchicalNamespace;
+  }
+
   /** Returns a builder for the current bucket. */
   public Builder toBuilder() {
     return new BuilderImpl(this);
@@ -2805,6 +2996,8 @@ public class BucketInfo implements Serializable {
         autoclass,
         locationType,
         objectRetention,
+        softDeletePolicy,
+        hierarchicalNamespace,
         logging);
   }
 
@@ -2846,6 +3039,8 @@ public class BucketInfo implements Serializable {
         && Objects.equals(autoclass, that.autoclass)
         && Objects.equals(locationType, that.locationType)
         && Objects.equals(objectRetention, that.objectRetention)
+        && Objects.equals(softDeletePolicy, that.softDeletePolicy)
+        && Objects.equals(hierarchicalNamespace, that.hierarchicalNamespace)
         && Objects.equals(logging, that.logging);
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
index 9ddf229c..28c51873 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.Storage.BucketField.SOFT_DELETE_POLICY;
 import static com.google.cloud.storage.Utils.bucketNameCodec;
 import static com.google.cloud.storage.Utils.ifNonNull;
 import static com.google.cloud.storage.Utils.lift;
@@ -90,6 +91,9 @@ final class GrpcConversions {
       Codec.of(this::iamConfigEncode, this::iamConfigDecode);
   private final Codec<BucketInfo.Autoclass, Bucket.Autoclass> autoclassCodec =
       Codec.of(this::autoclassEncode, this::autoclassDecode);
+
+  private final Codec<BucketInfo.SoftDeletePolicy, Bucket.SoftDeletePolicy> softDeletePolicyCodec =
+      Codec.of(this::softDeletePolicyEncode, this::softDeletePolicyDecode);
   private final Codec<BucketInfo.LifecycleRule, Bucket.Lifecycle.Rule> lifecycleRuleCodec =
       Codec.of(this::lifecycleRuleEncode, this::lifecycleRuleDecode);
   private final Codec<BucketInfo, Bucket> bucketInfoCodec =
@@ -110,6 +114,10 @@ final class GrpcConversions {
   private final Codec<Condition, Expr> iamConditionCodec =
       Codec.of(this::conditionEncode, this::conditionDecode);
 
+  private final Codec<BucketInfo.HierarchicalNamespace, Bucket.HierarchicalNamespace>
+      hierarchicalNamespaceCodec =
+          Codec.of(this::hierarchicalNamespaceEncode, this::hierarchicalNamespaceDecode);
+
   @VisibleForTesting
   final Codec<OffsetDateTime, Timestamp> timestampCodec =
       Codec.of(
@@ -290,6 +298,9 @@ final class GrpcConversions {
     if (from.hasAutoclass()) {
       to.setAutoclass(autoclassCodec.decode(from.getAutoclass()));
     }
+    if (from.hasSoftDeletePolicy()) {
+      to.setSoftDeletePolicy(softDeletePolicyCodec.decode(from.getSoftDeletePolicy()));
+    }
     if (from.hasCustomPlacementConfig()) {
       Bucket.CustomPlacementConfig customPlacementConfig = from.getCustomPlacementConfig();
       to.setCustomPlacementConfig(
@@ -297,6 +308,10 @@ final class GrpcConversions {
               .setDataLocations(customPlacementConfig.getDataLocationsList())
               .build());
     }
+    if (from.hasHierarchicalNamespace()) {
+      to.setHierarchicalNamespace(
+          hierarchicalNamespaceCodec.decode(from.getHierarchicalNamespace()));
+    }
     // TODO(frankyn): Add SelfLink when the field is available
     if (!from.getEtag().isEmpty()) {
       to.setEtag(from.getEtag());
@@ -375,6 +390,11 @@ final class GrpcConversions {
     ifNonNull(from.getAcl(), toImmutableListOf(bucketAclCodec::encode), to::addAllAcl);
     ifNonNull(from.getIamConfiguration(), iamConfigurationCodec::encode, to::setIamConfig);
     ifNonNull(from.getAutoclass(), autoclassCodec::encode, to::setAutoclass);
+    ifNonNull(from.getSoftDeletePolicy(), softDeletePolicyCodec::encode, to::setSoftDeletePolicy);
+    if (from.getModifiedFields().contains(SOFT_DELETE_POLICY)
+        && from.getSoftDeletePolicy() == null) {
+      to.clearSoftDeletePolicy();
+    }
     CustomPlacementConfig customPlacementConfig = from.getCustomPlacementConfig();
     if (customPlacementConfig != null && customPlacementConfig.getDataLocations() != null) {
       to.setCustomPlacementConfig(
@@ -382,6 +402,10 @@ final class GrpcConversions {
               .addAllDataLocations(customPlacementConfig.getDataLocations())
               .build());
     }
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        hierarchicalNamespaceCodec::encode,
+        to::setHierarchicalNamespace);
     // TODO(frankyn): Add SelfLink when the field is available
     ifNonNull(from.getEtag(), to::setEtag);
     return to.build();
@@ -589,6 +613,33 @@ final class GrpcConversions {
     return to.build();
   }
 
+  private BucketInfo.SoftDeletePolicy softDeletePolicyDecode(Bucket.SoftDeletePolicy from) {
+    BucketInfo.SoftDeletePolicy.Builder to = BucketInfo.SoftDeletePolicy.newBuilder();
+    ifNonNull(from.getRetentionDuration(), durationCodec::decode, to::setRetentionDuration);
+    ifNonNull(from.getEffectiveTime(), timestampCodec::decode, to::setEffectiveTime);
+    return to.build();
+  }
+
+  private Bucket.SoftDeletePolicy softDeletePolicyEncode(BucketInfo.SoftDeletePolicy from) {
+    Bucket.SoftDeletePolicy.Builder to = Bucket.SoftDeletePolicy.newBuilder();
+    ifNonNull(from.getRetentionDuration(), durationCodec::encode, to::setRetentionDuration);
+    return to.build();
+  }
+
+  private Bucket.HierarchicalNamespace hierarchicalNamespaceEncode(
+      BucketInfo.HierarchicalNamespace from) {
+    Bucket.HierarchicalNamespace.Builder to = Bucket.HierarchicalNamespace.newBuilder();
+    ifNonNull(from.getEnabled(), to::setEnabled);
+    return to.build();
+  }
+
+  private BucketInfo.HierarchicalNamespace hierarchicalNamespaceDecode(
+      Bucket.HierarchicalNamespace from) {
+    BucketInfo.HierarchicalNamespace.Builder to = BucketInfo.HierarchicalNamespace.newBuilder();
+    to.setEnabled(from.getEnabled());
+    return to.build();
+  }
+
   private Bucket.IamConfig iamConfigEncode(BucketInfo.IamConfiguration from) {
     Bucket.IamConfig.Builder to = Bucket.IamConfig.newBuilder();
     to.setUniformBucketLevelAccess(ublaEncode(from));
@@ -837,6 +888,8 @@ final class GrpcConversions {
     ifNonNull(from.getUpdateTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setUpdateTime);
     ifNonNull(from.getCreateTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setCreateTime);
     ifNonNull(from.getCustomTimeOffsetDateTime(), timestampCodec::encode, toBuilder::setCustomTime);
+    ifNonNull(from.getSoftDeleteTime(), timestampCodec::encode, toBuilder::setSoftDeleteTime);
+    ifNonNull(from.getHardDeleteTime(), timestampCodec::encode, toBuilder::setHardDeleteTime);
     ifNonNull(
         from.getCustomerEncryption(),
         customerEncryptionCodec::encode,
@@ -902,6 +955,12 @@ final class GrpcConversions {
     if (from.hasCustomerEncryption()) {
       toBuilder.setCustomerEncryption(customerEncryptionCodec.decode(from.getCustomerEncryption()));
     }
+    if (from.hasSoftDeleteTime()) {
+      toBuilder.setSoftDeleteTime(timestampCodec.decode(from.getSoftDeleteTime()));
+    }
+    if (from.hasHardDeleteTime()) {
+      toBuilder.setHardDeleteTime(timestampCodec.decode(from.getHardDeleteTime()));
+    }
     String storageClass = from.getStorageClass();
     if (!storageClass.isEmpty()) {
       toBuilder.setStorageClass(StorageClass.valueOf(storageClass));
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
index ea481ab2..de7af195 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcRetryAlgorithmManager.java
@@ -41,6 +41,7 @@ import com.google.storage.v2.ListObjectsRequest;
 import com.google.storage.v2.LockBucketRetentionPolicyRequest;
 import com.google.storage.v2.QueryWriteStatusRequest;
 import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.RestoreObjectRequest;
 import com.google.storage.v2.RewriteObjectRequest;
 import com.google.storage.v2.StartResumableWriteRequest;
 import com.google.storage.v2.UpdateBucketRequest;
@@ -123,6 +124,10 @@ final class GrpcRetryAlgorithmManager implements Serializable {
     return retryStrategy.getIdempotentHandler();
   }
 
+  public ResultRetryAlgorithm<?> getFor(RestoreObjectRequest req) {
+    return retryStrategy.getIdempotentHandler();
+  }
+
   public ResultRetryAlgorithm<?> getFor(GetServiceAccountRequest req) {
     return retryStrategy.getIdempotentHandler();
   }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index 53c73d4d..e9857e93 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -112,6 +112,7 @@ import com.google.storage.v2.NotificationConfigName;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
 import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.RestoreObjectRequest;
 import com.google.storage.v2.RewriteObjectRequest;
 import com.google.storage.v2.RewriteResponse;
 import com.google.storage.v2.StorageClient;
@@ -403,6 +404,33 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
     return get(blob, new BlobGetOption[0]);
   }
 
+  @Override
+  public Blob restore(BlobId blob, BlobRestoreOption... options) {
+    Opts<ObjectSourceOpt> unwrap = Opts.unwrap(options);
+    return internalObjectRestore(blob, unwrap);
+  }
+
+  private Blob internalObjectRestore(BlobId blobId, Opts<ObjectSourceOpt> opts) {
+    Opts<ObjectSourceOpt> finalOpts = opts.prepend(defaultOpts).prepend(ALL_BLOB_FIELDS);
+    GrpcCallContext grpcCallContext =
+        finalOpts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+    RestoreObjectRequest.Builder builder =
+        RestoreObjectRequest.newBuilder()
+            .setBucket(bucketNameCodec.encode(blobId.getBucket()))
+            .setObject(blobId.getName());
+    ifNonNull(blobId.getGeneration(), builder::setGeneration);
+    RestoreObjectRequest req = finalOpts.restoreObjectRequest().apply(builder).build();
+    GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> storageClient.restoreObjectCallable().call(req, merge),
+        resp -> {
+          BlobInfo tmp = codecs.blobInfo().decode(resp);
+          return finalOpts.clearBlobFields().decode(tmp).asBlob(this);
+        });
+  }
+
   @Override
   public Page<Bucket> list(BucketListOption... options) {
     Opts<BucketListOpt> opts = Opts.unwrap(options).prepend(defaultOpts).prepend(ALL_BUCKET_FIELDS);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
index 22cac9a7..c5163ad0 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpRetryAlgorithmManager.java
@@ -213,6 +213,11 @@ final class HttpRetryAlgorithmManager implements Serializable {
     return retryStrategy.getIdempotentHandler();
   }
 
+  public ResultRetryAlgorithm<?> getForObjectsRestore(
+      StorageObject pb, Map<StorageRpc.Option, ?> optionsMap) {
+    return retryStrategy.getIdempotentHandler();
+  }
+
   public ResultRetryAlgorithm<?> getForObjectsUpdate(
       StorageObject pb, Map<StorageRpc.Option, ?> optionsMap) {
     return optionsMap.containsKey(StorageRpc.Option.IF_METAGENERATION_MATCH)
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
index 4bbf08d4..7f84db77 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.Storage.BucketField.SOFT_DELETE_POLICY;
 import static com.google.cloud.storage.Utils.dateTimeCodec;
 import static com.google.cloud.storage.Utils.durationSecondsCodec;
 import static com.google.cloud.storage.Utils.ifNonNull;
@@ -66,6 +67,7 @@ import com.google.cloud.storage.BucketInfo.LifecycleRule.SetStorageClassLifecycl
 import com.google.cloud.storage.BucketInfo.Logging;
 import com.google.cloud.storage.BucketInfo.ObjectRetention;
 import com.google.cloud.storage.BucketInfo.PublicAccessPrevention;
+import com.google.cloud.storage.BucketInfo.SoftDeletePolicy;
 import com.google.cloud.storage.Conversions.Codec;
 import com.google.cloud.storage.Cors.Origin;
 import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
@@ -119,6 +121,9 @@ final class JsonConversions {
 
   private final Codec<ObjectRetention, Bucket.ObjectRetention> objectRetentionCodec =
       Codec.of(this::objectRetentionEncode, this::objectRetentionDecode);
+
+  private final Codec<SoftDeletePolicy, Bucket.SoftDeletePolicy> softDeletePolicyCodec =
+      Codec.of(this::softDeletePolicyEncode, this::softDeletePolicyDecode);
   private final Codec<LifecycleRule, Rule> lifecycleRuleCodec =
       Codec.of(this::lifecycleRuleEncode, this::lifecycleRuleDecode);
   private final Codec<LifecycleCondition, Condition> lifecycleConditionCodec =
@@ -137,6 +142,10 @@ final class JsonConversions {
   private final Codec<BlobInfo, StorageObject> blobInfoCodec =
       Codec.of(this::blobInfoEncode, this::blobInfoDecode);
 
+  private final Codec<BucketInfo.HierarchicalNamespace, Bucket.HierarchicalNamespace>
+      hierarchicalNamespaceCodec =
+          Codec.of(this::hierarchicalNamespaceEncode, this::hierarchicalNamespaceDecode);
+
   private final Codec<NotificationInfo, com.google.api.services.storage.model.Notification>
       notificationInfoCodec = Codec.of(this::notificationEncode, this::notificationDecode);
   private final Codec<CustomPlacementConfig, Bucket.CustomPlacementConfig>
@@ -247,6 +256,9 @@ final class JsonConversions {
         dateTimeCodec::encode,
         to::setRetentionExpirationTime);
 
+    ifNonNull(from.getSoftDeleteTime(), dateTimeCodec::encode, to::setSoftDeleteTime);
+    ifNonNull(from.getHardDeleteTime(), dateTimeCodec::encode, to::setHardDeleteTime);
+
     // todo: clean this up once retention is enabled in grpc
     // This is a workaround so that explicitly null retention objects are only included when the
     // user set an existing policy to null, to avoid sending any retention objects to the test
@@ -328,6 +340,8 @@ final class JsonConversions {
         dateTimeCodec::decode,
         to::setRetentionExpirationTimeOffsetDateTime);
     ifNonNull(from.getRetention(), this::retentionDecode, to::setRetention);
+    ifNonNull(from.getSoftDeleteTime(), dateTimeCodec::decode, to::setSoftDeleteTime);
+    ifNonNull(from.getHardDeleteTime(), dateTimeCodec::decode, to::setHardDeleteTime);
     return to.build();
   }
 
@@ -367,6 +381,21 @@ final class JsonConversions {
     return to.build();
   }
 
+  private Bucket.SoftDeletePolicy softDeletePolicyEncode(SoftDeletePolicy from) {
+    Bucket.SoftDeletePolicy to = new Bucket.SoftDeletePolicy();
+    ifNonNull(
+        from.getRetentionDuration(), durationSecondsCodec::encode, to::setRetentionDurationSeconds);
+    return to;
+  }
+
+  private SoftDeletePolicy softDeletePolicyDecode(Bucket.SoftDeletePolicy from) {
+    SoftDeletePolicy.Builder to = SoftDeletePolicy.newBuilder();
+    ifNonNull(
+        from.getRetentionDurationSeconds(), durationSecondsCodec::decode, to::setRetentionDuration);
+    ifNonNull(from.getEffectiveTime(), dateTimeCodec::decode, to::setEffectiveTime);
+    return to.build();
+  }
+
   private Bucket bucketInfoEncode(BucketInfo from) {
     Bucket to = new Bucket();
     ifNonNull(from.getProject(), projectNameCodec::encode, p -> to.set(PROJECT_ID_FIELD_NAME, p));
@@ -437,6 +466,15 @@ final class JsonConversions {
         this::customPlacementConfigEncode,
         to::setCustomPlacementConfig);
     ifNonNull(from.getObjectRetention(), this::objectRetentionEncode, to::setObjectRetention);
+    ifNonNull(from.getSoftDeletePolicy(), this::softDeletePolicyEncode, to::setSoftDeletePolicy);
+    if (from.getSoftDeletePolicy() == null
+        && from.getModifiedFields().contains(SOFT_DELETE_POLICY)) {
+      to.setSoftDeletePolicy(Data.nullOf(Bucket.SoftDeletePolicy.class));
+    }
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        this::hierarchicalNamespaceEncode,
+        to::setHierarchicalNamespace);
     return to;
   }
 
@@ -487,7 +525,12 @@ final class JsonConversions {
         from.getCustomPlacementConfig(),
         this::customPlacementConfigDecode,
         to::setCustomPlacementConfig);
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        this::hierarchicalNamespaceDecode,
+        to::setHierarchicalNamespace);
     ifNonNull(from.getObjectRetention(), this::objectRetentionDecode, to::setObjectRetention);
+    ifNonNull(from.getSoftDeletePolicy(), this::softDeletePolicyDecode, to::setSoftDeletePolicy);
     return to.build();
   }
 
@@ -861,6 +904,20 @@ final class JsonConversions {
     return to;
   }
 
+  private Bucket.HierarchicalNamespace hierarchicalNamespaceEncode(
+      BucketInfo.HierarchicalNamespace from) {
+    Bucket.HierarchicalNamespace to = new Bucket.HierarchicalNamespace();
+    ifNonNull(from.getEnabled(), to::setEnabled);
+    return to;
+  }
+
+  private BucketInfo.HierarchicalNamespace hierarchicalNamespaceDecode(
+      Bucket.HierarchicalNamespace from) {
+    BucketInfo.HierarchicalNamespace.Builder to = BucketInfo.HierarchicalNamespace.newBuilder();
+    to.setEnabled(from.getEnabled());
+    return to.build();
+  }
+
   private NotificationInfo notificationDecode(
       com.google.api.services.storage.model.Notification from) {
     NotificationInfo.Builder builder = new NotificationInfo.BuilderImpl(from.getTopic());
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
index b877d82d..07551abe 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfig.java
@@ -42,12 +42,15 @@ import java.io.Serializable;
 import java.nio.charset.StandardCharsets;
 import java.security.SecureRandom;
 import java.time.Clock;
+import java.time.Duration;
+import java.time.OffsetDateTime;
 import java.util.Base64;
 import java.util.Base64.Encoder;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.UnaryOperator;
 import javax.annotation.concurrent.Immutable;
 import org.checkerframework.checker.nullness.qual.NonNull;
 
@@ -125,18 +128,21 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
   private final BufferAllocationStrategy bufferAllocationStrategy;
   private final PartNamingStrategy partNamingStrategy;
   private final PartCleanupStrategy partCleanupStrategy;
+  private final PartMetadataFieldDecorator partMetadataFieldDecorator;
 
   private ParallelCompositeUploadBlobWriteSessionConfig(
       int maxPartsPerCompose,
       ExecutorSupplier executorSupplier,
       BufferAllocationStrategy bufferAllocationStrategy,
       PartNamingStrategy partNamingStrategy,
-      PartCleanupStrategy partCleanupStrategy) {
+      PartCleanupStrategy partCleanupStrategy,
+      PartMetadataFieldDecorator partMetadataFieldDecorator) {
     this.maxPartsPerCompose = maxPartsPerCompose;
     this.executorSupplier = executorSupplier;
     this.bufferAllocationStrategy = bufferAllocationStrategy;
     this.partNamingStrategy = partNamingStrategy;
     this.partCleanupStrategy = partCleanupStrategy;
+    this.partMetadataFieldDecorator = partMetadataFieldDecorator;
   }
 
   @InternalApi
@@ -150,7 +156,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         executorSupplier,
         bufferAllocationStrategy,
         partNamingStrategy,
-        partCleanupStrategy);
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
   }
 
   /**
@@ -170,7 +177,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         executorSupplier,
         bufferAllocationStrategy,
         partNamingStrategy,
-        partCleanupStrategy);
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
   }
 
   /**
@@ -191,7 +199,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         executorSupplier,
         bufferAllocationStrategy,
         partNamingStrategy,
-        partCleanupStrategy);
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
   }
 
   /**
@@ -211,7 +220,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         executorSupplier,
         bufferAllocationStrategy,
         partNamingStrategy,
-        partCleanupStrategy);
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
   }
 
   /**
@@ -231,7 +241,29 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         executorSupplier,
         bufferAllocationStrategy,
         partNamingStrategy,
-        partCleanupStrategy);
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
+  }
+
+  /**
+   * Specify a Part Metadata Field decorator, this will manipulate the metadata associated with part
+   * objects, the ultimate object metadata will remain unchanged.
+   *
+   * <p><i>Default: </i> {@link PartMetadataFieldDecorator#noOp()}
+   *
+   * @since 2.36.0 This new api is in preview and is subject to breaking changes.
+   */
+  @BetaApi
+  public ParallelCompositeUploadBlobWriteSessionConfig withPartMetadataFieldDecorator(
+      PartMetadataFieldDecorator partMetadataFieldDecorator) {
+    checkNotNull(partMetadataFieldDecorator, "partMetadataFieldDecorator must be non null");
+    return new ParallelCompositeUploadBlobWriteSessionConfig(
+        maxPartsPerCompose,
+        executorSupplier,
+        bufferAllocationStrategy,
+        partNamingStrategy,
+        partCleanupStrategy,
+        partMetadataFieldDecorator);
   }
 
   @BetaApi
@@ -241,7 +273,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
         ExecutorSupplier.cachedPool(),
         BufferAllocationStrategy.simple(ByteSizeConstants._16MiB),
         PartNamingStrategy.noPrefix(),
-        PartCleanupStrategy.always());
+        PartCleanupStrategy.always(),
+        PartMetadataFieldDecorator.noOp());
   }
 
   @InternalApi
@@ -249,7 +282,10 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
   WriterFactory createFactory(Clock clock) throws IOException {
     Executor executor = executorSupplier.get();
     BufferHandlePool bufferHandlePool = bufferAllocationStrategy.get();
-    return new ParallelCompositeUploadWriterFactory(clock, executor, bufferHandlePool);
+    PartMetadataFieldDecoratorInstance partMetadataFieldDecoratorInstance =
+        partMetadataFieldDecorator.newInstance(clock);
+    return new ParallelCompositeUploadWriterFactory(
+        clock, executor, bufferHandlePool, partMetadataFieldDecoratorInstance);
   }
 
   /**
@@ -277,6 +313,7 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
      */
     @BetaApi
     public static BufferAllocationStrategy simple(int capacity) {
+      checkArgument(capacity > 0, "bufferCapacity must be > 0");
       return new SimpleBufferAllocationStrategy(capacity);
     }
 
@@ -291,6 +328,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
      */
     @BetaApi
     public static BufferAllocationStrategy fixedPool(int bufferCount, int bufferCapacity) {
+      checkArgument(bufferCount > 0, "bufferCount must be > 0");
+      checkArgument(bufferCapacity > 0, "bufferCapacity must be > 0");
       return new FixedPoolBufferAllocationStrategy(bufferCount, bufferCapacity);
     }
 
@@ -361,6 +400,7 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
      */
     @BetaApi
     public static ExecutorSupplier fixedPool(int poolSize) {
+      checkArgument(poolSize > 0, "poolSize must be > 0");
       return new FixedSupplier(poolSize);
     }
 
@@ -612,7 +652,10 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
       }
 
       @Override
-      protected String fmtFields(String randomKey, String nameDigest, String partRange) {
+      protected String fmtFields(String randomKey, String ultimateObjectName, String partRange) {
+        HashCode hashCode =
+            OBJECT_NAME_HASH_FUNCTION.hashString(ultimateObjectName, StandardCharsets.UTF_8);
+        String nameDigest = B64.encodeToString(hashCode.asBytes());
         return randomKey
             + FIELD_SEPARATOR
             // todo: do we want to
@@ -628,6 +671,79 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
     }
   }
 
+  /**
+   * A Decorator which is used to manipulate metadata fields, specifically on the part objects
+   * created in a Parallel Composite Upload
+   *
+   * @see #withPartMetadataFieldDecorator(PartMetadataFieldDecorator)
+   * @since 2.36.0 This new api is in preview and is subject to breaking changes.
+   */
+  @BetaApi
+  @Immutable
+  public abstract static class PartMetadataFieldDecorator implements Serializable {
+
+    abstract PartMetadataFieldDecoratorInstance newInstance(Clock clock);
+
+    /**
+     * A decorator that is used to manipulate the Custom Time Metadata field of part files. {@link
+     * BlobInfo#getCustomTimeOffsetDateTime()}
+     *
+     * <p>When provided with a duration, a time in the future will be calculated for each part
+     * object upon upload, this new value can be used in OLM rules to cleanup abandoned part files.
+     *
+     * <p>See [CustomTime OLM
+     * documentation](https://cloud.google.com/storage/docs/lifecycle#dayssincecustomtime)
+     *
+     * @see #withPartMetadataFieldDecorator(PartMetadataFieldDecorator)
+     * @since 2.36.0 This new api is in preview and is subject to breaking changes.
+     */
+    @BetaApi
+    public static PartMetadataFieldDecorator setCustomTimeInFuture(Duration timeInFuture) {
+      checkNotNull(timeInFuture, "timeInFuture must not be null");
+      return new CustomTimeInFuture(timeInFuture);
+    }
+
+    @BetaApi
+    public static PartMetadataFieldDecorator noOp() {
+      return NoOp.INSTANCE;
+    }
+
+    @BetaApi
+    private static final class CustomTimeInFuture extends PartMetadataFieldDecorator {
+      private static final long serialVersionUID = -6213742554954751892L;
+      private final Duration duration;
+
+      CustomTimeInFuture(Duration duration) {
+        this.duration = duration;
+      }
+
+      @Override
+      PartMetadataFieldDecoratorInstance newInstance(Clock clock) {
+        return builder -> {
+          OffsetDateTime futureTime =
+              OffsetDateTime.from(
+                  clock.instant().plus(duration).atZone(clock.getZone()).toOffsetDateTime());
+          return builder.setCustomTimeOffsetDateTime(futureTime);
+        };
+      }
+    }
+
+    private static final class NoOp extends PartMetadataFieldDecorator {
+      private static final long serialVersionUID = -4569486383992999205L;
+      private static final NoOp INSTANCE = new NoOp();
+
+      @Override
+      PartMetadataFieldDecoratorInstance newInstance(Clock clock) {
+        return builder -> builder;
+      }
+
+      /** prevent java serialization from using a new instance */
+      private Object readResolve() {
+        return INSTANCE;
+      }
+    }
+  }
+
   /**
    * A cleanup strategy which will dictate what cleanup operations are performed automatically when
    * performing a parallel composite upload.
@@ -705,6 +821,8 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
     }
   }
 
+  interface PartMetadataFieldDecoratorInstance extends UnaryOperator<BlobInfo.Builder> {}
+
   private abstract static class Factory<T> implements Serializable {
     private static final long serialVersionUID = 271806144227661056L;
 
@@ -718,12 +836,17 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
     private final Clock clock;
     private final Executor executor;
     private final BufferHandlePool bufferHandlePool;
+    private final PartMetadataFieldDecoratorInstance partMetadataFieldDecoratorInstance;
 
     private ParallelCompositeUploadWriterFactory(
-        Clock clock, Executor executor, BufferHandlePool bufferHandlePool) {
+        Clock clock,
+        Executor executor,
+        BufferHandlePool bufferHandlePool,
+        PartMetadataFieldDecoratorInstance partMetadataFieldDecoratorInstance) {
       this.clock = clock;
       this.executor = executor;
       this.bufferHandlePool = bufferHandlePool;
+      this.partMetadataFieldDecoratorInstance = partMetadataFieldDecoratorInstance;
     }
 
     @Override
@@ -757,6 +880,7 @@ public final class ParallelCompositeUploadBlobWriteSessionConfig extends BlobWri
                 partNamingStrategy,
                 partCleanupStrategy,
                 maxPartsPerCompose,
+                partMetadataFieldDecoratorInstance,
                 result,
                 storageInternal,
                 info,
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannel.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannel.java
index 9ff1ebdb..639802cd 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannel.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannel.java
@@ -31,6 +31,7 @@ import com.google.cloud.storage.BufferHandlePool.PooledBuffer;
 import com.google.cloud.storage.BufferedWritableByteChannelSession.BufferedWritableByteChannel;
 import com.google.cloud.storage.MetadataField.PartRange;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecoratorInstance;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
 import com.google.cloud.storage.Storage.ComposeRequest;
 import com.google.cloud.storage.UnifiedOpts.Crc32cMatch;
@@ -111,6 +112,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
   private final PartNamingStrategy partNamingStrategy;
   private final PartCleanupStrategy partCleanupStrategy;
   private final int maxElementsPerCompact;
+  private final PartMetadataFieldDecoratorInstance partMetadataFieldDecorator;
   private final SettableApiFuture<BlobInfo> finalObject;
   private final StorageInternal storage;
   private final BlobInfo ultimateObject;
@@ -135,6 +137,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
       PartNamingStrategy partNamingStrategy,
       PartCleanupStrategy partCleanupStrategy,
       int maxElementsPerCompact,
+      PartMetadataFieldDecoratorInstance partMetadataFieldDecorator,
       SettableApiFuture<BlobInfo> finalObject,
       StorageInternal storage,
       BlobInfo ultimateObject,
@@ -144,6 +147,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
     this.partNamingStrategy = partNamingStrategy;
     this.partCleanupStrategy = partCleanupStrategy;
     this.maxElementsPerCompact = maxElementsPerCompact;
+    this.partMetadataFieldDecorator = partMetadataFieldDecorator;
     this.finalObject = finalObject;
     this.storage = storage;
     this.ultimateObject = ultimateObject;
@@ -427,6 +431,7 @@ final class ParallelCompositeUploadWritableByteChannel implements BufferedWritab
     PART_INDEX.appendTo(partRange, builder);
     OBJECT_OFFSET.appendTo(offset, builder);
     b.setMetadata(builder.build());
+    b = partMetadataFieldDecorator.apply(b);
     return b.build();
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 5ca31c04..0f6b002d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -159,8 +159,14 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     CUSTOM_PLACEMENT_CONFIG("customPlacementConfig", "custom_placement_config"),
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     AUTOCLASS("autoclass"),
+
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    HIERARCHICAL_NAMESPACE("hierarchicalNamespace", "hierarchical_namespace"),
     @TransportCompatibility({Transport.HTTP})
-    OBJECT_RETENTION("objectRetention");
+    OBJECT_RETENTION("objectRetention"),
+
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    SOFT_DELETE_POLICY("softDeletePolicy", "soft_delete_policy");
 
     static final List<BucketField> REQUIRED_FIELDS = ImmutableList.of(NAME);
 
@@ -260,7 +266,13 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     CUSTOMER_ENCRYPTION("customerEncryption", "customer_encryption"),
     @TransportCompatibility({Transport.HTTP})
-    RETENTION("retention");
+    RETENTION("retention"),
+
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    SOFT_DELETE_TIME("softDeleteTime", "soft_delete_time"),
+
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    HARD_DELETE_TIME("hardDeleteTime", "hard_delete_time");
 
     static final List<NamedField> REQUIRED_FIELDS = ImmutableList.of(BUCKET, NAME);
 
@@ -1569,6 +1581,16 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobGetOption(UnifiedOpts.returnRawInputStream(shouldReturnRawInputStream));
     }
 
+    /**
+     * Returns an option for whether the request should return a soft-deleted object. If an object
+     * has been soft-deleted (Deleted while a Soft Delete Policy) is in place, this must be true or
+     * the request will return null.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobGetOption softDeleted(boolean softDeleted) {
+      return new BlobGetOption(UnifiedOpts.softDeleted(softDeleted));
+    }
+
     /**
      * Deduplicate any options which are the same parameter. The value which comes last in {@code
      * os} will be the value included in the return.
@@ -1604,6 +1626,61 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     }
   }
 
+  /** Class for specifying blob restore options * */
+  class BlobRestoreOption extends Option<ObjectSourceOpt> {
+
+    private static final long serialVersionUID = 1922118465380110958L;
+
+    BlobRestoreOption(ObjectSourceOpt opt) {
+      super(opt);
+    }
+
+    /**
+     * Returns an option for blob's data generation match. If this option is used the request will
+     * fail if generation does not match.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobRestoreOption generationMatch(long generation) {
+      return new BlobRestoreOption(UnifiedOpts.generationMatch(generation));
+    }
+
+    /**
+     * Returns an option for blob's data generation mismatch. If this option is used the request
+     * will fail if blob's generation matches the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobRestoreOption generationNotMatch(long generation) {
+      return new BlobRestoreOption(UnifiedOpts.generationNotMatch(generation));
+    }
+
+    /**
+     * Returns an option for blob's metageneration match. If this option is used the request will
+     * fail if blob's metageneration does not match the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobRestoreOption metagenerationMatch(long generation) {
+      return new BlobRestoreOption(UnifiedOpts.metagenerationMatch(generation));
+    }
+
+    /**
+     * Returns an option for blob's metageneration mismatch. If this option is used the request will
+     * fail if blob's metageneration matches the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobRestoreOption metagenerationNotMatch(long generation) {
+      return new BlobRestoreOption(UnifiedOpts.metagenerationNotMatch(generation));
+    }
+
+    /**
+     * Returns an option for whether the restored object should copy the access controls of the
+     * source object.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobRestoreOption copySourceAcl(boolean copySourceAcl) {
+      return new BlobRestoreOption(UnifiedOpts.copySourceAcl(copySourceAcl));
+    }
+  }
+
   /** Class for specifying bucket list options. */
   class BucketListOption extends Option<BucketListOpt> {
 
@@ -1788,6 +1865,14 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobListOption(UnifiedOpts.matchGlob(glob));
     }
 
+    /**
+     * Returns an option for whether to include all Folders (including empty Folders) in response.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobListOption includeFolders(boolean includeFolders) {
+      return new BlobListOption(UnifiedOpts.includeFoldersAsPrefixes(includeFolders));
+    }
+
     /**
      * Returns an option to define the billing user project. This option is required by buckets with
      * `requester_pays` flag enabled to assign operation costs.
@@ -1827,6 +1912,12 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobListOption(UnifiedOpts.fields(set));
     }
 
+    /** Returns an option for whether the list result should include soft-deleted objects. */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobListOption softDeleted(boolean softDeleted) {
+      return new BlobListOption(UnifiedOpts.softDeleted(softDeleted));
+    }
+
     /**
      * Deduplicate any options which are the same parameter. The value which comes last in {@code
      * os} will be the value included in the return.
@@ -3032,6 +3123,23 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
   @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Blob get(BlobId blob);
 
+  /**
+   * Restores a soft-deleted object to full object status and returns the object. Note that you must
+   * specify a generation to use this method.
+   *
+   * <p>Example of restoring an object.
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String blobName = "my-blob-name";
+   * long generation = 42;
+   * BlobId blobId = BlobId.of(bucketName, blobName, gen);
+   * Blob blob = storage.restore(blobId);
+   * }</pre>
+   */
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+  Blob restore(BlobId blob, BlobRestoreOption... options);
+
   /**
    * Lists the project's buckets.
    *
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index 9af54fee..a5df68b8 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -341,6 +341,24 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     return get(blob, new BlobGetOption[0]);
   }
 
+  @Override
+  public Blob restore(BlobId blob, BlobRestoreOption... options) {
+    ImmutableMap<StorageRpc.Option, ?> optionsMap =
+        Opts.unwrap(options).resolveFrom(blob).getRpcOptions();
+
+    StorageObject obj = codecs.blobId().encode(blob);
+
+    ResultRetryAlgorithm<?> algorithm = retryAlgorithmManager.getForObjectsRestore(obj, optionsMap);
+
+    return run(
+        algorithm,
+        () -> storageRpc.restore(obj, optionsMap),
+        (x) -> {
+          BlobInfo info = Conversions.json().blobInfo().decode(x);
+          return info.asBlob(this);
+        });
+  }
+
   private static class BucketPageFetcher implements NextPageFetcher<Bucket> {
 
     private static final long serialVersionUID = 8534413447247364038L;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index dbf104e9..06909620 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -53,6 +53,7 @@ import com.google.storage.v2.ListHmacKeysRequest;
 import com.google.storage.v2.ListObjectsRequest;
 import com.google.storage.v2.LockBucketRetentionPolicyRequest;
 import com.google.storage.v2.ReadObjectRequest;
+import com.google.storage.v2.RestoreObjectRequest;
 import com.google.storage.v2.RewriteObjectRequest;
 import com.google.storage.v2.UpdateBucketRequest;
 import com.google.storage.v2.UpdateHmacKeyRequest;
@@ -157,6 +158,10 @@ final class UnifiedOpts {
     default Mapper<RewriteObjectRequest.Builder> rewriteObject() {
       return Mapper.identity();
     }
+
+    default Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return Mapper.identity();
+    }
   }
 
   /**
@@ -370,6 +375,10 @@ final class UnifiedOpts {
     return new Delimiter(delimiter);
   }
 
+  static IncludeFoldersAsPrefixes includeFoldersAsPrefixes(boolean includeFoldersAsPrefixes) {
+    return new IncludeFoldersAsPrefixes(includeFoldersAsPrefixes);
+  }
+
   @Deprecated
   static DetectContentType detectContentType() {
     return DetectContentType.INSTANCE;
@@ -478,6 +487,14 @@ final class UnifiedOpts {
     return new Projection(projection);
   }
 
+  static SoftDeleted softDeleted(boolean softDeleted) {
+    return new SoftDeleted(softDeleted);
+  }
+
+  static CopySourceAcl copySourceAcl(boolean copySourceAcl) {
+    return new CopySourceAcl(copySourceAcl);
+  }
+
   static RequestedPolicyVersion requestedPolicyVersion(long l) {
     return new RequestedPolicyVersion(l);
   }
@@ -636,6 +653,20 @@ final class UnifiedOpts {
     }
   }
 
+  static final class IncludeFoldersAsPrefixes extends RpcOptVal<Boolean> implements ObjectListOpt {
+
+    private static final long serialVersionUID = 321916692864878282L;
+
+    private IncludeFoldersAsPrefixes(boolean val) {
+      super(StorageRpc.Option.INCLUDE_FOLDERS_AS_PREFIXES, val);
+    }
+
+    @Override
+    public Mapper<ListObjectsRequest.Builder> listObjects() {
+      return b -> b.setIncludeFoldersAsPrefixes(val);
+    }
+  }
+
   static final class Delimiter extends RpcOptVal<String> implements ObjectListOpt {
     private static final long serialVersionUID = -3789556789947615714L;
 
@@ -649,6 +680,40 @@ final class UnifiedOpts {
     }
   }
 
+  static final class SoftDeleted extends RpcOptVal<Boolean>
+      implements ObjectListOpt, ObjectSourceOpt {
+
+    private static final long serialVersionUID = -8526951678111463350L;
+
+    private SoftDeleted(boolean val) {
+      super(StorageRpc.Option.SOFT_DELETED, val);
+    }
+
+    @Override
+    public Mapper<ListObjectsRequest.Builder> listObjects() {
+      return b -> b.setSoftDeleted(val);
+    }
+
+    @Override
+    public Mapper<GetObjectRequest.Builder> getObject() {
+      return b -> b.setSoftDeleted(val);
+    }
+  }
+
+  static final class CopySourceAcl extends RpcOptVal<Boolean> implements ObjectSourceOpt {
+
+    private static final long serialVersionUID = 2033755749149128119L;
+
+    private CopySourceAcl(boolean val) {
+      super(StorageRpc.Option.COPY_SOURCE_ACL, val);
+    }
+
+    @Override
+    public Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return b -> b.setCopySourceAcl(val);
+    }
+  }
+
   static final class DisableGzipContent extends RpcOptVal<@NonNull Boolean>
       implements ObjectTargetOpt {
     private static final long serialVersionUID = 7445066765944965549L;
@@ -990,6 +1055,11 @@ final class UnifiedOpts {
       return b -> b.setIfGenerationMatch(val);
     }
 
+    @Override
+    public Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return b -> b.setIfGenerationMatch(val);
+    }
+
     @Override
     public Mapper<UpdateObjectRequest.Builder> updateObject() {
       return b -> b.setIfGenerationMatch(val);
@@ -1046,6 +1116,11 @@ final class UnifiedOpts {
       return b -> b.setIfGenerationNotMatch(val);
     }
 
+    @Override
+    public Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return b -> b.setIfGenerationNotMatch(val);
+    }
+
     @Override
     public Mapper<UpdateObjectRequest.Builder> updateObject() {
       return b -> b.setIfGenerationNotMatch(val);
@@ -1187,6 +1262,11 @@ final class UnifiedOpts {
       return b -> b.setIfMetagenerationMatch(val);
     }
 
+    @Override
+    public Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return b -> b.setIfMetagenerationMatch(val);
+    }
+
     @Override
     public Mapper<UpdateObjectRequest.Builder> updateObject() {
       return b -> b.setIfMetagenerationMatch(val);
@@ -1267,6 +1347,11 @@ final class UnifiedOpts {
       return b -> b.setIfMetagenerationNotMatch(val);
     }
 
+    @Override
+    public Mapper<RestoreObjectRequest.Builder> restoreObject() {
+      return b -> b.setIfMetagenerationNotMatch(val);
+    }
+
     @Override
     public Mapper<UpdateObjectRequest.Builder> updateObject() {
       return b -> b.setIfMetagenerationNotMatch(val);
@@ -2279,6 +2364,10 @@ final class UnifiedOpts {
       return fuseMappers(ObjectSourceOpt.class, ObjectSourceOpt::getObject);
     }
 
+    Mapper<RestoreObjectRequest.Builder> restoreObjectRequest() {
+      return fuseMappers(ObjectSourceOpt.class, ObjectSourceOpt::restoreObject);
+    }
+
     Mapper<ReadObjectRequest.Builder> readObjectRequest() {
       return fuseMappers(ObjectSourceOpt.class, ObjectSourceOpt::readObject);
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index 2f1c3e21..8a973427 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -459,6 +459,8 @@ public class HttpStorageRpc implements StorageRpc {
               .setPageToken(Option.PAGE_TOKEN.getString(options))
               .setFields(Option.FIELDS.getString(options))
               .setUserProject(Option.USER_PROJECT.getString(options))
+              .setSoftDeleted(Option.SOFT_DELETED.getBoolean(options))
+              .setIncludeFoldersAsPrefixes(Option.INCLUDE_FOLDERS_AS_PREFIXES.getBoolean(options))
               .execute();
       Iterable<StorageObject> storageObjects =
           Iterables.concat(
@@ -540,7 +542,8 @@ public class HttpStorageRpc implements StorageRpc {
         .setIfGenerationMatch(Option.IF_GENERATION_MATCH.getLong(options))
         .setIfGenerationNotMatch(Option.IF_GENERATION_NOT_MATCH.getLong(options))
         .setFields(Option.FIELDS.getString(options))
-        .setUserProject(Option.USER_PROJECT.getString(options));
+        .setUserProject(Option.USER_PROJECT.getString(options))
+        .setSoftDeleted(Option.SOFT_DELETED.getBoolean(options));
   }
 
   @Override
@@ -562,6 +565,36 @@ public class HttpStorageRpc implements StorageRpc {
     }
   }
 
+  @Override
+  public StorageObject restore(StorageObject object, Map<Option, ?> options) {
+    Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_RESTORE_OBJECT);
+    Scope scope = tracer.withSpan(span);
+    try {
+      Storage.Objects.Restore restore =
+          storage.objects().restore(object.getBucket(), object.getName(), object.getGeneration());
+      return restore
+          .setProjection(DEFAULT_PROJECTION)
+          .setIfMetagenerationMatch(Option.IF_METAGENERATION_MATCH.getLong(options))
+          .setIfMetagenerationNotMatch(Option.IF_METAGENERATION_NOT_MATCH.getLong(options))
+          .setIfGenerationMatch(Option.IF_GENERATION_MATCH.getLong(options))
+          .setIfGenerationNotMatch(Option.IF_GENERATION_NOT_MATCH.getLong(options))
+          .setCopySourceAcl(Option.COPY_SOURCE_ACL.getBoolean(options))
+          .setUserProject(Option.USER_PROJECT.getString(options))
+          .setFields(Option.FIELDS.getString(options))
+          .execute();
+    } catch (IOException ex) {
+      span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
+      StorageException serviceException = translate(ex);
+      if (serviceException.getCode() == HTTP_NOT_FOUND) {
+        return null;
+      }
+      throw serviceException;
+    } finally {
+      scope.close();
+      span.end(HttpStorageRpcSpans.END_SPAN_OPTIONS);
+    }
+  }
+
   @Override
   public Bucket patch(Bucket bucket, Map<Option, ?> options) {
     Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_PATCH_BUCKET);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
index 3f3d27d9..dc4b0533 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
@@ -30,6 +30,7 @@ class HttpStorageRpcSpans {
   static final String SPAN_NAME_LIST_OBJECTS = getTraceSpanName("list(String,Map)");
   static final String SPAN_NAME_GET_BUCKET = getTraceSpanName("get(Bucket,Map)");
   static final String SPAN_NAME_GET_OBJECT = getTraceSpanName("get(StorageObject,Map)");
+  static final String SPAN_NAME_RESTORE_OBJECT = getTraceSpanName("restore(StorageObject, Map)");
   static final String SPAN_NAME_PATCH_BUCKET = getTraceSpanName("patch(Bucket,Map)");
   static final String SPAN_NAME_PATCH_OBJECT = getTraceSpanName("patch(StorageObject,Map)");
   static final String SPAN_NAME_DELETE_BUCKET = getTraceSpanName("delete(Bucket,Map)");
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
index 7d671dee..d4e0abbf 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
@@ -73,7 +73,11 @@ public interface StorageRpc extends ServiceRpc {
     DETECT_CONTENT_TYPE("detectContentType"),
     ENABLE_OBJECT_RETENTION("enableObjectRetention"),
     RETURN_RAW_INPUT_STREAM("returnRawInputStream"),
-    OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention");
+    OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention"),
+    SOFT_DELETED("softDeleted"),
+    COPY_SOURCE_ACL("copySourceAcl"),
+    GENERATION("generation"),
+    INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes");
 
     private final String value;
 
@@ -241,6 +245,13 @@ public interface StorageRpc extends ServiceRpc {
    */
   StorageObject get(StorageObject object, Map<Option, ?> options);
 
+  /**
+   * If an object has been soft-deleted, restores it and returns the restored object.j
+   *
+   * @throws StorageException upon failure
+   */
+  StorageObject restore(StorageObject object, Map<Option, ?> options);
+
   /**
    * Updates bucket information.
    *
diff --git a/grpc-google-cloud-storage-control-v2/pom.xml b/grpc-google-cloud-storage-control-v2/pom.xml
index 1b9b70b4..469f69ee 100644
--- a/grpc-google-cloud-storage-control-v2/pom.xml
+++ b/grpc-google-cloud-storage-control-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-  <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+  <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
   <name>grpc-google-cloud-storage-control-v2</name>
   <description>GRPC library for google-cloud-storage</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index a8f61837..8657a91e 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index 98845612..5aee33f8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -14,7 +14,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>sdk-platform-java-config</artifactId>
-    <version>3.27.0</version>
+    <version>3.28.1</version>
   </parent>
 
   <developers>
@@ -76,12 +76,12 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
-        <version>v1-rev20240307-2.0.0</version>
+        <version>v1-rev20240311-2.0.0</version>
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
@@ -117,32 +117,32 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-control</artifactId>
-        <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+        <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-control-v2/pom.xml b/proto-google-cloud-storage-control-v2/pom.xml
index f028d71d..7324731e 100644
--- a/proto-google-cloud-storage-control-v2/pom.xml
+++ b/proto-google-cloud-storage-control-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-  <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+  <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
   <name>proto-google-cloud-storage-control-v2</name>
   <description>Proto library for  proto-google-cloud-storage-control-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageControlProto.java b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageControlProto.java
index 796f5f0c..4d08cb63 100644
--- a/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageControlProto.java
+++ b/proto-google-cloud-storage-control-v2/src/main/java/com/google/storage/control/v2/StorageControlProto.java
@@ -204,12 +204,15 @@ public final class StorageControlProto {
           + "m/auth/devstorage.full_control,https://w"
           + "ww.googleapis.com/auth/devstorage.read_o"
           + "nly,https://www.googleapis.com/auth/devs"
-          + "torage.read_writeB\312\001\n\035com.google.storage"
+          + "torage.read_writeB\264\002\n\035com.google.storage"
           + ".control.v2B\023StorageControlProtoP\001ZKclou"
           + "d.google.com/go/storage/control/apiv2/st"
-          + "oragecontrolpb;storagecontrolpb\352AD\n\035stor"
-          + "age.googleapis.com/Bucket\022#projects/{pro"
-          + "ject}/buckets/{bucket}b\006proto3"
+          + "oragecontrolpb;storagecontrolpb\252\002\037Google"
+          + ".Cloud.Storage.Control.V2\312\002\037Google\\Cloud"
+          + "\\Storage\\Control\\V2\352\002#Google::Cloud::Sto"
+          + "rage::Control::V2\352AD\n\035storage.googleapis"
+          + ".com/Bucket\022#projects/{project}/buckets/"
+          + "{bucket}b\006proto3"
     };
     descriptor =
         com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(
diff --git a/proto-google-cloud-storage-control-v2/src/main/proto/google/storage/control/v2/storage_control.proto b/proto-google-cloud-storage-control-v2/src/main/proto/google/storage/control/v2/storage_control.proto
index 65a01f39..ddc391c5 100644
--- a/proto-google-cloud-storage-control-v2/src/main/proto/google/storage/control/v2/storage_control.proto
+++ b/proto-google-cloud-storage-control-v2/src/main/proto/google/storage/control/v2/storage_control.proto
@@ -25,10 +25,13 @@ import "google/longrunning/operations.proto";
 import "google/protobuf/empty.proto";
 import "google/protobuf/timestamp.proto";
 
+option csharp_namespace = "Google.Cloud.Storage.Control.V2";
 option go_package = "cloud.google.com/go/storage/control/apiv2/storagecontrolpb;storagecontrolpb";
 option java_multiple_files = true;
 option java_outer_classname = "StorageControlProto";
 option java_package = "com.google.storage.control.v2";
+option php_namespace = "Google\\Cloud\\Storage\\Control\\V2";
+option ruby_package = "Google::Cloud::Storage::Control::V2";
 option (google.api.resource_definition) = {
   type: "storage.googleapis.com/Bucket"
   pattern: "projects/{project}/buckets/{bucket}"
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index 6dcaf944..3f3e50e0 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.35.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.36.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 2f8aca59..ef39f872 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.35.0</version>
+      <version>2.36.0</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 6211bd42..ec36aef4 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
diff --git a/storage-shared-benchmarking/pom.xml b/storage-shared-benchmarking/pom.xml
index 5bb63020..b7c22550 100644
--- a/storage-shared-benchmarking/pom.xml
+++ b/storage-shared-benchmarking/pom.xml
@@ -10,7 +10,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
 
   <properties>
@@ -31,7 +31,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.35.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.36.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       <classifier>tests</classifier>
     </dependency>
     <dependency>
diff --git a/versions.txt b/versions.txt
index 1ed68144..44521568 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,10 +1,10 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.35.0:2.35.1-SNAPSHOT
-gapic-google-cloud-storage-v2:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
-google-cloud-storage-control:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
-proto-google-cloud-storage-control-v2:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
-grpc-google-cloud-storage-control-v2:2.35.0-alpha:2.35.1-alpha-SNAPSHOT
+google-cloud-storage:2.36.0:2.36.1-SNAPSHOT
+gapic-google-cloud-storage-v2:2.36.0-alpha:2.36.1-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.36.0-alpha:2.36.1-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.36.0-alpha:2.36.1-alpha-SNAPSHOT
+google-cloud-storage-control:2.36.0-alpha:2.36.1-alpha-SNAPSHOT
+proto-google-cloud-storage-control-v2:2.36.0-alpha:2.36.1-alpha-SNAPSHOT
+grpc-google-cloud-storage-control-v2:2.36.0-alpha:2.36.1-alpha-SNAPSHOT


Test patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
index 97104634..6686cb92 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
@@ -71,6 +71,11 @@ public class StorageRpcTestBase implements StorageRpc {
     throw new UnsupportedOperationException("Not implemented yet");
   }
 
+  @Override
+  public StorageObject restore(StorageObject object, Map<Option, ?> options) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
   @Override
   public Bucket patch(Bucket bucket, Map<Option, ?> options) {
     throw new UnsupportedOperationException("Not implemented yet");
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
index 86eddc5b..3ef55372 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadBlobWriteSessionConfigTest.java
@@ -21,8 +21,13 @@ import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.cloud.storage.MetadataField.PartRange;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecorator;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
 import com.google.common.truth.StringSubject;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import org.junit.Test;
 
 public final class ParallelCompositeUploadBlobWriteSessionConfigTest {
@@ -84,7 +89,19 @@ public final class ParallelCompositeUploadBlobWriteSessionConfigTest {
         // name digest
         () -> assertField(fmt, 1).hasLength(22),
         () -> assertField(fmt, 2).isEqualTo("0001-0096.part"),
-        () -> assertThat(fmt).startsWith("a/b/obj/"));
+        () -> assertThat(fmt).startsWith("a/b/obj"));
+  }
+
+  @Test
+  public void partMetadataFieldDecorator_customTime() {
+    BlobInfo.Builder testBlob = BlobInfo.newBuilder("testBlob", "testBucket");
+    Duration duration = Duration.ofSeconds(30);
+    TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1));
+    OffsetDateTime expected =
+        OffsetDateTime.from(Instant.EPOCH.plus(duration).atZone(ZoneId.of("Z")));
+    PartMetadataFieldDecorator.setCustomTimeInFuture(duration).newInstance(clock).apply(testBlob);
+
+    assertThat(expected).isEqualTo(testBlob.build().getCustomTimeOffsetDateTime());
   }
 
   private static StringSubject assertField(String fmt, int idx) {
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannelTest.java
index ed66a3f1..3a78adf4 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannelTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ParallelCompositeUploadWritableByteChannelTest.java
@@ -29,6 +29,8 @@ import com.google.cloud.storage.BufferHandlePool.PooledBuffer;
 import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
 import com.google.cloud.storage.MetadataField.PartRange;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecorator;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecoratorInstance;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
 import com.google.cloud.storage.ParallelCompositeUploadWritableByteChannel.BufferHandleReleaser;
 import com.google.cloud.storage.Storage.ComposeRequest;
@@ -47,9 +49,14 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.common.util.concurrent.Uninterruptibles;
 import com.google.storage.v2.WriteObjectRequest;
 import io.grpc.Status.Code;
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousCloseException;
 import java.nio.channels.ClosedChannelException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Collections;
@@ -80,6 +87,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
   private SettableApiFuture<BlobInfo> finalObject;
   private FakeStorageInternal storageInternal;
   private SimplisticPartNamingStrategy partNamingStrategy;
+  private PartMetadataFieldDecoratorInstance partMetadataFieldDecorator;
   private int bufferCapacity;
 
   @Before
@@ -91,6 +99,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
     finalObject = SettableApiFuture.create();
     partNamingStrategy = new SimplisticPartNamingStrategy("prefix");
     storageInternal = new FakeStorageInternal();
+    partMetadataFieldDecorator = PartMetadataFieldDecorator.noOp().newInstance(null);
   }
 
   @Test
@@ -202,6 +211,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.never(),
             maxElementsPerCompact,
+            partMetadataFieldDecorator,
             finalObject,
             storageInternal,
             info,
@@ -241,6 +251,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.never(),
             maxElementsPerCompact,
+            partMetadataFieldDecorator,
             finalObject,
             storageInternal,
             info,
@@ -340,6 +351,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.never(),
             3,
+            partMetadataFieldDecorator,
             finalObject,
             new FakeStorageInternal() {
               @Override
@@ -429,6 +441,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.always(),
             3,
+            partMetadataFieldDecorator,
             finalObject,
             new FakeStorageInternal() {
               @Override
@@ -462,6 +475,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.always(),
             3,
+            partMetadataFieldDecorator,
             finalObject,
             new FakeStorageInternal() {
               @Override
@@ -568,6 +582,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
               partNamingStrategy,
               PartCleanupStrategy.never(),
               32,
+              partMetadataFieldDecorator,
               finalObject,
               storageInternal,
               info,
@@ -647,6 +662,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.never(),
             3,
+            partMetadataFieldDecorator,
             finalObject,
             new FakeStorageInternal() {
               @Override
@@ -729,6 +745,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
             partNamingStrategy,
             PartCleanupStrategy.always(),
             10,
+            partMetadataFieldDecorator,
             finalObject,
             storageInternal,
             info,
@@ -749,6 +766,49 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
         () -> assertThat(storageInternal.deleteRequests).containsExactly(p1, p2, p3));
   }
 
+  @Test
+  public void partMetadataFieldDecoratorUsesCustomTime() throws IOException {
+    TestClock clock = TestClock.tickBy(Instant.EPOCH, Duration.ofSeconds(1));
+    OffsetDateTime rangeBegin =
+        OffsetDateTime.from(Instant.EPOCH.plus(Duration.ofSeconds(29)).atZone(ZoneId.of("Z")));
+    OffsetDateTime rangeEnd =
+        OffsetDateTime.from(Instant.EPOCH.plus(Duration.ofMinutes(2)).atZone(ZoneId.of("Z")));
+
+    FakeStorageInternal storageInternal =
+        new FakeStorageInternal() {
+          @Override
+          public BlobInfo internalDirectUpload(
+              BlobInfo info, Opts<ObjectTargetOpt> opts, ByteBuffer buf) {
+            if (info.getBlobId().getName().endsWith(".part")) {
+              // Kinda hacky but since we are creating multiple parts we will use a range
+              // to ensure the customTimes are being calculated appropriately
+              assertThat(info.getCustomTimeOffsetDateTime().isAfter(rangeBegin)).isTrue();
+              assertThat(info.getCustomTimeOffsetDateTime().isBefore(rangeEnd)).isTrue();
+            } else {
+              assertThat(info.getCustomTimeOffsetDateTime()).isNull();
+            }
+            return super.internalDirectUpload(info, opts, buf);
+          }
+        };
+    ParallelCompositeUploadWritableByteChannel pcu =
+        new ParallelCompositeUploadWritableByteChannel(
+            bufferHandlePool,
+            MoreExecutors.directExecutor(),
+            partNamingStrategy,
+            PartCleanupStrategy.always(),
+            10,
+            PartMetadataFieldDecorator.setCustomTimeInFuture(Duration.ofSeconds(30))
+                .newInstance(clock),
+            finalObject,
+            storageInternal,
+            info,
+            opts);
+    byte[] bytes = DataGenerator.base64Characters().genBytes(bufferCapacity * 3 - 1);
+    pcu.write(ByteBuffer.wrap(bytes));
+
+    pcu.close();
+  }
+
   @NonNull
   private ParallelCompositeUploadWritableByteChannel defaultPcu(int maxElementsPerCompact) {
     return new ParallelCompositeUploadWritableByteChannel(
@@ -757,6 +817,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
         partNamingStrategy,
         PartCleanupStrategy.always(),
         maxElementsPerCompact,
+        partMetadataFieldDecorator,
         finalObject,
         storageInternal,
         info,
@@ -773,7 +834,7 @@ public final class ParallelCompositeUploadWritableByteChannelTest {
     protected final List<ComposeRequest> composeRequests;
     protected final List<BlobId> deleteRequests;
 
-    private FakeStorageInternal() {
+    FakeStorageInternal() {
       generations = new AtomicInteger(1);
       addedObjects = Collections.synchronizedMap(new HashMap<>());
       composeRequests = Collections.synchronizedList(new ArrayList<>());
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java
index eab0e78e..5f642b50 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/SerializationTest.java
@@ -36,6 +36,7 @@ import com.google.cloud.storage.BlobWriteChannelV2.BlobWriteChannelV2State;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.BufferAllocationStrategy;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.ExecutorSupplier;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecorator;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
 import com.google.cloud.storage.Storage.BlobTargetOption;
 import com.google.cloud.storage.Storage.BucketField;
@@ -55,6 +56,7 @@ import java.io.InvalidClassException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.time.Duration;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.Map;
@@ -388,10 +390,16 @@ public class SerializationTest extends BaseSerializationTest {
             .withBufferAllocationStrategy(BufferAllocationStrategy.fixedPool(1, 3))
             .withPartCleanupStrategy(PartCleanupStrategy.never())
             .withPartNamingStrategy(PartNamingStrategy.prefix("prefix"))
-            .withExecutorSupplier(ExecutorSupplier.fixedPool(5));
+            .withExecutorSupplier(ExecutorSupplier.fixedPool(5))
+            .withPartMetadataFieldDecorator(
+                PartMetadataFieldDecorator.setCustomTimeInFuture(Duration.ofMinutes(10)));
     ParallelCompositeUploadBlobWriteSessionConfig pcu2copy = serializeAndDeserialize(pcu2);
     assertThat(pcu2copy).isNotNull();
 
+    PartMetadataFieldDecorator noop = PartMetadataFieldDecorator.noOp();
+    PartMetadataFieldDecorator noopCopy = serializeAndDeserialize(noop);
+    assertThat(noopCopy).isSameInstanceAs(noop);
+
     InvalidClassException invalidClassException =
         assertThrows(
             InvalidClassException.class,
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java
index e37a4f02..89d04c35 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestClock.java
@@ -37,7 +37,7 @@ final class TestClock extends Clock {
 
   @Override
   public ZoneId getZone() {
-    throw new UnsupportedOperationException("TestClock.getZone()");
+    return ZoneId.of("Z");
   }
 
   @Override
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java
index c2594a48..bf8c4825 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java
@@ -201,7 +201,9 @@ public final class ITBlobReadMaskTest {
               new Args<>(BlobField.UPDATED, LazyAssertion.equal()),
               new Args<>(
                   BlobField.RETENTION,
-                  LazyAssertion.skip("TODO: jesse fill in buganizer bug here")));
+                  LazyAssertion.skip("TODO: jesse fill in buganizer bug here")),
+              new Args<>(BlobField.SOFT_DELETE_TIME, LazyAssertion.equal()),
+              new Args<>(BlobField.HARD_DELETE_TIME, LazyAssertion.equal()));
       List<String> argsDefined =
           args.stream().map(Args::getField).map(Enum::name).sorted().collect(Collectors.toList());
 
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
index 93099d41..c4c3c405 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
@@ -40,6 +40,7 @@ import com.google.cloud.storage.it.runner.annotations.Parameterized.ParametersPr
 import com.google.cloud.storage.it.runner.annotations.SingleBackend;
 import com.google.cloud.storage.it.runner.annotations.StorageFixture;
 import com.google.common.collect.ImmutableList;
+import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -133,7 +134,27 @@ public final class ITBucketReadMaskTest {
               new Args<>(BucketField.TIME_CREATED, LazyAssertion.equal()),
               new Args<>(BucketField.UPDATED, LazyAssertion.equal()),
               new Args<>(BucketField.VERSIONING, LazyAssertion.equal()),
-              new Args<>(BucketField.WEBSITE, LazyAssertion.equal()));
+              new Args<>(BucketField.WEBSITE, LazyAssertion.equal()),
+              new Args<>(
+                  BucketField.SOFT_DELETE_POLICY,
+                  (jsonT, grpcT) -> {
+                    assertThat(
+                        jsonT
+                            .getSoftDeletePolicy()
+                            .getRetentionDuration()
+                            .equals(grpcT.getSoftDeletePolicy().getRetentionDuration()));
+                    assertThat(
+                        jsonT
+                            .getSoftDeletePolicy()
+                            .getEffectiveTime()
+                            .truncatedTo(ChronoUnit.SECONDS)
+                            .equals(
+                                grpcT
+                                    .getSoftDeletePolicy()
+                                    .getEffectiveTime()
+                                    .truncatedTo(ChronoUnit.SECONDS)));
+                  }),
+              new Args<>(BucketField.HIERARCHICAL_NAMESPACE, LazyAssertion.equal()));
 
       List<String> argsDefined =
           args.stream().map(Args::getField).map(Enum::name).sorted().collect(Collectors.toList());
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
index e32be5c3..97ab8ad3 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.gax.paging.Page;
+import com.google.api.services.storage.model.Folder;
 import com.google.cloud.Policy;
 import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.BlobInfo;
@@ -44,6 +45,7 @@ import com.google.cloud.storage.Storage.BucketGetOption;
 import com.google.cloud.storage.Storage.BucketListOption;
 import com.google.cloud.storage.Storage.BucketTargetOption;
 import com.google.cloud.storage.StorageClass;
+import com.google.cloud.storage.StorageOptions;
 import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
@@ -53,6 +55,7 @@ import com.google.cloud.storage.it.runner.annotations.BucketType;
 import com.google.cloud.storage.it.runner.annotations.CrossRun;
 import com.google.cloud.storage.it.runner.annotations.Inject;
 import com.google.cloud.storage.it.runner.registry.Generator;
+import com.google.cloud.storage.spi.v1.HttpStorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.time.Duration;
@@ -551,6 +554,140 @@ public class ITBucketTest {
     }
   }
 
+  @Test
+  public void testSoftDeletePolicy() {
+    String bucketName = generator.randomBucketName();
+    BucketInfo bucketInfo =
+        BucketInfo.newBuilder(bucketName)
+            .setSoftDeletePolicy(
+                BucketInfo.SoftDeletePolicy.newBuilder()
+                    .setRetentionDuration(Duration.ofDays(10))
+                    .build())
+            .build();
+    try {
+      storage.create(bucketInfo);
+
+      Bucket remoteBucket = storage.get(bucketName);
+      assertEquals(Duration.ofDays(10), remoteBucket.getSoftDeletePolicy().getRetentionDuration());
+      assertNotNull(remoteBucket.getSoftDeletePolicy().getEffectiveTime());
+
+      String softDelBlobName = "softdelblob";
+      remoteBucket.create(softDelBlobName, BLOB_BYTE_CONTENT);
+
+      Blob blob = remoteBucket.get(softDelBlobName);
+      long gen = blob.getGeneration();
+
+      assertNull(blob.getSoftDeleteTime());
+      assertNull(blob.getHardDeleteTime());
+
+      blob.delete();
+
+      assertNull(remoteBucket.get(softDelBlobName));
+
+      ImmutableList<Blob> softDeletedBlobs =
+          ImmutableList.copyOf(
+              remoteBucket.list(Storage.BlobListOption.softDeleted(true)).iterateAll());
+      assertThat(softDeletedBlobs.size() > 0);
+
+      Blob softDeletedBlob =
+          remoteBucket.get(softDelBlobName, gen, Storage.BlobGetOption.softDeleted(true));
+
+      assertNotNull(softDeletedBlob);
+      assertNotNull(softDeletedBlob.getSoftDeleteTime());
+      assertNotNull(softDeletedBlob.getHardDeleteTime());
+
+      assertNotNull(storage.restore(softDeletedBlob.getBlobId()));
+
+      remoteBucket
+          .toBuilder()
+          .setSoftDeletePolicy(
+              BucketInfo.SoftDeletePolicy.newBuilder()
+                  .setRetentionDuration(Duration.ofDays(20))
+                  .build())
+          .build()
+          .update();
+
+      assertEquals(
+          Duration.ofDays(20),
+          storage.get(bucketName).getSoftDeletePolicy().getRetentionDuration());
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
+  @Test
+  public void createBucketWithHierarchicalNamespace() {
+    String bucketName = generator.randomBucketName();
+    storage.create(
+        BucketInfo.newBuilder(bucketName)
+            .setHierarchicalNamespace(
+                BucketInfo.HierarchicalNamespace.newBuilder().setEnabled(true).build())
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(true)
+                    .build())
+            .build());
+    try {
+      Bucket remoteBucket = storage.get(bucketName);
+      assertNotNull(remoteBucket.getHierarchicalNamespace());
+      assertTrue(remoteBucket.getHierarchicalNamespace().getEnabled());
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
+  @Test
+  public void testListObjectsWithFolders() throws Exception {
+    String bucketName = generator.randomBucketName();
+    storage.create(
+        BucketInfo.newBuilder(bucketName)
+            .setHierarchicalNamespace(
+                BucketInfo.HierarchicalNamespace.newBuilder().setEnabled(true).build())
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(true)
+                    .build())
+            .build());
+    try {
+      com.google.api.services.storage.Storage apiaryStorage =
+          new HttpStorageRpc(StorageOptions.getDefaultInstance()).getStorage();
+      apiaryStorage
+          .folders()
+          .insert(bucketName, new Folder().setName("F").setBucket(bucketName))
+          .execute();
+
+      Page<Blob> blobs =
+          storage.list(
+              bucketName,
+              Storage.BlobListOption.delimiter("/"),
+              Storage.BlobListOption.includeFolders(false));
+
+      boolean found = false;
+      for (Blob blob : blobs.iterateAll()) {
+        if (blob.getName().equals("F/")) {
+          found = true;
+        }
+      }
+      assert (!found);
+
+      blobs =
+          storage.list(
+              bucketName,
+              Storage.BlobListOption.delimiter("/"),
+              Storage.BlobListOption.includeFolders(true));
+
+      for (Blob blob : blobs.iterateAll()) {
+        if (blob.getName().equals("F/")) {
+          found = true;
+        }
+      }
+      assert (found);
+
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
   private void unsetRequesterPays() {
     Bucket remoteBucket =
         storage.get(
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
index 4dbe55ad..c7032ea6 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
@@ -326,6 +326,9 @@ public final class ITOptionRegressionTest {
             "timeCreated",
             "updated",
             "versioning",
+            "website",
+            "softDeletePolicy",
+            "hierarchicalNamespace",
             "website");
     s.get(
         b.getName(),
@@ -738,7 +741,9 @@ public final class ITOptionRegressionTest {
             "timeDeleted",
             "timeStorageClassUpdated",
             "updated",
-            "retention");
+            "retention",
+            "softDeleteTime",
+            "hardDeleteTime");
     s.get(o.getBlobId(), BlobGetOption.fields(BlobField.values()));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }
@@ -816,6 +821,9 @@ public final class ITOptionRegressionTest {
             "items/timeCreated",
             "items/updated",
             "items/versioning",
+            "items/website",
+            "items/softDeletePolicy",
+            "items/hierarchicalNamespace",
             "items/website");
     s.list(BucketListOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
@@ -913,7 +921,9 @@ public final class ITOptionRegressionTest {
             "items/timeDeleted",
             "items/timeStorageClassUpdated",
             "items/updated",
-            "items/retention");
+            "items/retention",
+            "items/softDeleteTime",
+            "items/hardDeleteTime");
     s.list(b.getName(), BlobListOption.fields(BlobField.values()));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
index 012a700f..304cdd69 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITParallelCompositeUploadBlobWriteSessionConfigTest.java
@@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.assertThrows;
 
+import com.google.api.gax.paging.Page;
 import com.google.api.gax.rpc.ApiExceptions;
 import com.google.cloud.kms.v1.CryptoKey;
 import com.google.cloud.storage.Blob;
@@ -34,6 +35,7 @@ import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.BufferAllocationStrategy;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.ExecutorSupplier;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartCleanupStrategy;
+import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartMetadataFieldDecorator;
 import com.google.cloud.storage.ParallelCompositeUploadBlobWriteSessionConfig.PartNamingStrategy;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobSourceOption;
@@ -57,6 +59,7 @@ import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousCloseException;
 import java.nio.channels.WritableByteChannel;
 import java.security.Key;
+import java.time.Duration;
 import java.util.Random;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -109,8 +112,11 @@ public final class ITParallelCompositeUploadBlobWriteSessionConfigTest {
             // define a max part size that is fairly small to aid in test speed
             .withBufferAllocationStrategy(BufferAllocationStrategy.simple(_1MiB))
             .withPartNamingStrategy(PartNamingStrategy.prefix("prefix-a"))
-            // let our fixtures take care of cleaning things up if an upload fails
-            .withPartCleanupStrategy(PartCleanupStrategy.onlyOnSuccess());
+            // Write customTime 30 seconds in the future
+            .withPartMetadataFieldDecorator(
+                PartMetadataFieldDecorator.setCustomTimeInFuture(Duration.ofSeconds(30)))
+            // let our fixtures take care of cleaning things
+            .withPartCleanupStrategy(PartCleanupStrategy.never());
 
     StorageOptions storageOptions = null;
     if (transport == Transport.GRPC) {
@@ -140,6 +146,15 @@ public final class ITParallelCompositeUploadBlobWriteSessionConfigTest {
     }
   }
 
+  @Test
+  public void partFilesCreatedWithCustomTimeWritten() throws IOException {
+    doTest(bucket, 10 * _1MiB + 37, ImmutableList.of(), ImmutableList.of(), ImmutableList.of());
+    Page<Blob> blobs = storage.list(bucket.getName(), Storage.BlobListOption.prefix("prefix-a"));
+    for (Blob blob : blobs.iterateAll()) {
+      assertThat(blob.getCustomTimeOffsetDateTime()).isNotNull();
+    }
+  }
+
   @Test
   public void errorRaisedByMethodAndFutureResult() throws IOException {
 
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
index d264e5a6..9e5e9691 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
@@ -139,6 +139,11 @@ abstract class AbstractStorageProxy implements Storage {
     return delegate.get(blob);
   }
 
+  @Override
+  public Blob restore(BlobId blob, BlobRestoreOption... options) {
+    return delegate.restore(blob, options);
+  }
+
   @Override
   public Page<Bucket> list(BucketListOption... options) {
     return delegate.list(options);