Repository: googleapis/java-storage
Pull request #2315 (2023-11-29 17:09:23)

deps: update actions/setup-java action to v4
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-java](https://togithub.com/actions/setup-java) | action | major | `v3` -> `v4` |

---

### Release Notes

<details>
<summary>actions/setup-java (actions/setup-java)</summary>

### [`v4`](https://togithub.com/actions/setup-java/compare/v3...v4)

[Compare Source](https://togithub.com/actions/setup-java/compare/v3...v4)

</details>

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzNy41OS44IiwidXBkYXRlZEluVmVyIjoiMzcuNTkuOCIsInRhcmdldEJyYW5jaCI6Im1haW4ifQ==-->


forking-renovate[bot]: ### Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

 ⚠ **Warning**: custom changes will be lost.

Patch:
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index b3e4b99f..ba323387 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -15,6 +15,12 @@
     <method>* writeAndClose(*)</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>
+
   <!-- @BetaApi members -->
   <difference>
     <differenceType>7009</differenceType>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index e98b9273..62e676d1 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
-    <pubsub-proto.version>1.107.11</pubsub-proto.version>
+    <pubsub-proto.version>1.107.12</pubsub-proto.version>
     <junit-platform.version>5.10.1</junit-platform.version>
   </properties>
   <dependencies>
@@ -248,7 +248,7 @@
     <dependency>
       <groupId>net.jqwik</groupId>
       <artifactId>jqwik</artifactId>
-      <version>1.8.1</version>
+      <version>1.8.2</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 4bba8aef..e6295b8c 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,12 @@ public class Blob extends BlobInfo {
       return this;
     }
 
+    @Override
+    public Builder setRetention(Retention retention) {
+      infoBuilder.setRetention(retention);
+      return this;
+    }
+
     @Override
     public Blob build() {
       return new Blob(storage, infoBuilder);
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 288de2b3..e808f444 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
@@ -22,7 +22,10 @@ import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.client.util.Data;
+import com.google.api.core.ApiFunction;
 import com.google.api.core.BetaApi;
+import com.google.cloud.StringEnumType;
+import com.google.cloud.StringEnumValue;
 import com.google.cloud.storage.Storage.BlobField;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.UnifiedOpts.NamedField;
@@ -104,6 +107,7 @@ public class BlobInfo implements Serializable {
   private final Boolean eventBasedHold;
   private final Boolean temporaryHold;
   private final OffsetDateTime retentionExpirationTime;
+  private final Retention retention;
   private final transient ImmutableSet<NamedField> modifiedFields;
 
   /** This class is meant for internal use only. Users are discouraged from using this class. */
@@ -168,6 +172,119 @@ public class BlobInfo implements Serializable {
     }
   }
 
+  /**
+   * Defines a blob's Retention policy. Can only be used on objects in a retention-enabled bucket.
+   */
+  public static final class Retention implements Serializable {
+
+    private static final long serialVersionUID = 5046718464542688444L;
+
+    private Mode mode;
+
+    private OffsetDateTime retainUntilTime;
+
+    /** Returns the retention policy's Mode. Can be Locked or Unlocked. */
+    public Mode getMode() {
+      return mode;
+    }
+
+    /** Returns what time this object will be retained until, if the mode is Locked. */
+    public OffsetDateTime getRetainUntilTime() {
+      return retainUntilTime;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof Retention)) {
+        return false;
+      }
+      Retention that = (Retention) o;
+      return Objects.equals(mode, that.mode)
+          && Objects.equals(retainUntilTime, that.retainUntilTime);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(mode, retainUntilTime);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("mode", mode)
+          .add("retainUntilTime", retainUntilTime)
+          .toString();
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public Builder toBuilder() {
+      return new Builder().setMode(this.mode).setRetainUntilTime(this.retainUntilTime);
+    }
+
+    private Retention() {}
+
+    public Retention(Builder builder) {
+      this.mode = builder.mode;
+      this.retainUntilTime = builder.retainUntilTime;
+    }
+
+    public static final class Builder {
+      private Mode mode;
+      private OffsetDateTime retainUntilTime;
+
+      /** Sets the retention policy's Mode. Can be Locked or Unlocked. */
+      public Builder setMode(Mode mode) {
+        this.mode = mode;
+        return this;
+      }
+
+      /** Sets what time this object will be retained until, if the mode is Locked. */
+      public Builder setRetainUntilTime(OffsetDateTime retainUntilTime) {
+        this.retainUntilTime = retainUntilTime;
+        return this;
+      }
+
+      public Retention build() {
+        return new Retention(this);
+      }
+    }
+
+    public static final class Mode extends StringEnumValue {
+      private static final long serialVersionUID = 1973143582659557184L;
+
+      private Mode(String constant) {
+        super(constant);
+      }
+
+      private static final ApiFunction<String, Mode> CONSTRUCTOR = Mode::new;
+
+      private static final StringEnumType<Mode> type =
+          new StringEnumType<>(Mode.class, CONSTRUCTOR);
+
+      public static final Mode UNLOCKED = type.createAndRegister("Unlocked");
+
+      public static final Mode LOCKED = type.createAndRegister("Locked");
+
+      public static Mode valueOfStrict(String constant) {
+        return type.valueOfStrict(constant);
+      }
+
+      public static Mode valueOf(String constant) {
+        return type.valueOf(constant);
+      }
+
+      public static Mode[] values() {
+        return type.values();
+      }
+    }
+  }
+
   /** Builder for {@code BlobInfo}. */
   public abstract static class Builder {
 
@@ -408,6 +525,8 @@ public class BlobInfo implements Serializable {
       return setRetentionExpirationTime(millisOffsetDateTimeCodec.decode(retentionExpirationTime));
     }
 
+    public abstract Builder setRetention(Retention retention);
+
     /** Creates a {@code BlobInfo} object. */
     public abstract BlobInfo build();
 
@@ -506,6 +625,7 @@ public class BlobInfo implements Serializable {
     private Boolean eventBasedHold;
     private Boolean temporaryHold;
     private OffsetDateTime retentionExpirationTime;
+    private Retention retention;
     private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
 
     BuilderImpl(BlobId blobId) {
@@ -543,6 +663,7 @@ public class BlobInfo implements Serializable {
       eventBasedHold = blobInfo.eventBasedHold;
       temporaryHold = blobInfo.temporaryHold;
       retentionExpirationTime = blobInfo.retentionExpirationTime;
+      retention = blobInfo.retention;
     }
 
     @Override
@@ -916,6 +1037,14 @@ public class BlobInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setRetention(Retention retention) {
+      // todo: b/308194853
+      modifiedFields.add(BlobField.RETENTION);
+      this.retention = retention;
+      return this;
+    }
+
     @Override
     public BlobInfo build() {
       checkNotNull(blobId);
@@ -1139,6 +1268,7 @@ public class BlobInfo implements Serializable {
     eventBasedHold = builder.eventBasedHold;
     temporaryHold = builder.temporaryHold;
     retentionExpirationTime = builder.retentionExpirationTime;
+    retention = builder.retention;
     modifiedFields = builder.modifiedFields.build();
   }
 
@@ -1532,6 +1662,11 @@ public class BlobInfo implements Serializable {
     return retentionExpirationTime;
   }
 
+  /** Returns the object's Retention policy. */
+  public Retention getRetention() {
+    return retention;
+  }
+
   /** Returns a builder for the current blob. */
   public Builder toBuilder() {
     return new BuilderImpl(this);
@@ -1581,6 +1716,7 @@ public class BlobInfo implements Serializable {
         kmsKeyName,
         eventBasedHold,
         temporaryHold,
+        retention,
         retentionExpirationTime);
   }
 
@@ -1622,7 +1758,8 @@ public class BlobInfo implements Serializable {
         && Objects.equals(kmsKeyName, blobInfo.kmsKeyName)
         && Objects.equals(eventBasedHold, blobInfo.eventBasedHold)
         && Objects.equals(temporaryHold, blobInfo.temporaryHold)
-        && Objects.equals(retentionExpirationTime, blobInfo.retentionExpirationTime);
+        && Objects.equals(retentionExpirationTime, blobInfo.retentionExpirationTime)
+        && Objects.equals(retention, blobInfo.retention);
   }
 
   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 c8827c1f..3c1652bc 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
@@ -272,6 +272,16 @@ public class Bucket extends BucketInfo {
       return new BlobTargetOption(UnifiedOpts.userProject(userProject));
     }
 
+    /**
+     * Returns an option for overriding an Unlocked Retention policy. This must be set to true in
+     * order to change a policy from Unlocked to Locked, to set it to null, or to reduce its
+     * retainUntilTime attribute.
+     */
+    @TransportCompatibility({Transport.HTTP})
+    public static BlobTargetOption overrideUnlockedRetention(boolean overrideUnlockedRetention) {
+      return new BlobTargetOption(UnifiedOpts.overrideUnlockedRetention(overrideUnlockedRetention));
+    }
+
     /**
      * Deduplicate any options which are the same parameter. The value which comes last in {@code
      * os} will be the value included in the return.
@@ -732,6 +742,12 @@ public class Bucket extends BucketInfo {
       return this;
     }
 
+    @Override
+    Builder setObjectRetention(ObjectRetention objectRetention) {
+      infoBuilder.setObjectRetention(objectRetention);
+      return this;
+    }
+
     @Override
     public Bucket build() {
       return new Bucket(storage, infoBuilder);
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 5750b555..9fb46b5b 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
@@ -27,8 +27,11 @@ import static com.google.common.collect.Lists.newArrayList;
 import com.google.api.client.json.jackson2.JacksonFactory;
 import com.google.api.client.util.Data;
 import com.google.api.client.util.DateTime;
+import com.google.api.core.ApiFunction;
 import com.google.api.core.BetaApi;
 import com.google.api.services.storage.model.Bucket.Lifecycle.Rule;
+import com.google.cloud.StringEnumType;
+import com.google.cloud.StringEnumValue;
 import com.google.cloud.storage.Acl.Entity;
 import com.google.cloud.storage.BlobInfo.ImmutableEmptyMap;
 import com.google.cloud.storage.Storage.BucketField;
@@ -114,6 +117,8 @@ public class BucketInfo implements Serializable {
   private final String locationType;
   private final Logging logging;
   private final CustomPlacementConfig customPlacementConfig;
+  private final ObjectRetention objectRetention;
+
   private final transient ImmutableSet<NamedField> modifiedFields;
 
   /**
@@ -480,6 +485,95 @@ public class BucketInfo implements Serializable {
     }
   }
 
+  public static final class ObjectRetention implements Serializable {
+
+    private static final long serialVersionUID = 3948199339534287669L;
+    private Mode mode;
+
+    public Mode getMode() {
+      return mode;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof ObjectRetention)) {
+        return false;
+      }
+      ObjectRetention that = (ObjectRetention) o;
+      return Objects.equals(mode, that.mode);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(mode);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("mode", mode).toString();
+    }
+
+    private ObjectRetention() {}
+
+    private ObjectRetention(Builder builder) {
+      this.mode = builder.mode;
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public Builder toBuilder() {
+      return new Builder().setMode(this.mode);
+    }
+
+    public static final class Builder {
+      private Mode mode;
+
+      /** Sets the object retention mode. Can be Enabled or Disabled. */
+      public Builder setMode(Mode mode) {
+        this.mode = mode;
+        return this;
+      }
+
+      public ObjectRetention build() {
+        return new ObjectRetention(this);
+      }
+    }
+
+    public static final class Mode extends StringEnumValue {
+      private static final long serialVersionUID = 1973143582659557184L;
+
+      private Mode(String constant) {
+        super(constant);
+      }
+
+      private static final ApiFunction<String, Mode> CONSTRUCTOR = Mode::new;
+
+      private static final StringEnumType<Mode> type =
+          new StringEnumType<>(Mode.class, CONSTRUCTOR);
+
+      public static final Mode ENABLED = type.createAndRegister("Enabled");
+
+      public static final Mode DISABLED = type.createAndRegister("Disabled");
+
+      public static Mode valueOfStrict(String constant) {
+        return type.valueOfStrict(constant);
+      }
+
+      public static Mode valueOf(String constant) {
+        return type.valueOf(constant);
+      }
+
+      public static Mode[] values() {
+        return type.values();
+      }
+    }
+  }
+
   /**
    * The bucket's custom placement configuration for Custom Dual Regions. If using `location` is
    * also required.
@@ -1589,6 +1683,8 @@ public class BucketInfo implements Serializable {
 
     public abstract Builder setCustomPlacementConfig(CustomPlacementConfig customPlacementConfig);
 
+    abstract Builder setObjectRetention(ObjectRetention objectRetention);
+
     /** Creates a {@code BucketInfo} object. */
     public abstract BucketInfo build();
 
@@ -1686,6 +1782,7 @@ public class BucketInfo implements Serializable {
     private String locationType;
     private Logging logging;
     private CustomPlacementConfig customPlacementConfig;
+    private ObjectRetention objectRetention;
     private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
 
     BuilderImpl(String name) {
@@ -1724,6 +1821,7 @@ public class BucketInfo implements Serializable {
       locationType = bucketInfo.locationType;
       logging = bucketInfo.logging;
       customPlacementConfig = bucketInfo.customPlacementConfig;
+      objectRetention = bucketInfo.objectRetention;
     }
 
     @Override
@@ -2080,6 +2178,15 @@ public class BucketInfo implements Serializable {
       return this;
     }
 
+    @Override
+    Builder setObjectRetention(ObjectRetention objectRetention) {
+      if (!Objects.equals(this.objectRetention, objectRetention)) {
+        modifiedFields.add(BucketField.OBJECT_RETENTION);
+      }
+      this.objectRetention = objectRetention;
+      return this;
+    }
+
     @Override
     Builder setLocationType(String locationType) {
       if (!Objects.equals(this.locationType, locationType)) {
@@ -2320,6 +2427,7 @@ public class BucketInfo implements Serializable {
     locationType = builder.locationType;
     logging = builder.logging;
     customPlacementConfig = builder.customPlacementConfig;
+    objectRetention = builder.objectRetention;
     modifiedFields = builder.modifiedFields.build();
   }
 
@@ -2655,6 +2763,11 @@ public class BucketInfo implements Serializable {
     return customPlacementConfig;
   }
 
+  /** returns the Object Retention configuration */
+  public ObjectRetention getObjectRetention() {
+    return objectRetention;
+  }
+
   /** Returns a builder for the current bucket. */
   public Builder toBuilder() {
     return new BuilderImpl(this);
@@ -2691,6 +2804,7 @@ public class BucketInfo implements Serializable {
         iamConfiguration,
         autoclass,
         locationType,
+        objectRetention,
         logging);
   }
 
@@ -2731,6 +2845,7 @@ public class BucketInfo implements Serializable {
         && Objects.equals(iamConfiguration, that.iamConfiguration)
         && Objects.equals(autoclass, that.autoclass)
         && Objects.equals(locationType, that.locationType)
+        && Objects.equals(objectRetention, that.objectRetention)
         && Objects.equals(logging, that.logging);
   }
 
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 516748dc..a5613ff6 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
@@ -135,6 +135,7 @@ import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -169,7 +170,12 @@ final class GrpcStorageImpl extends BaseService<StorageOptions>
   private static final Opts<Fields> ALL_BLOB_FIELDS =
       Opts.from(UnifiedOpts.fields(ImmutableSet.copyOf(BlobField.values())));
   private static final Opts<Fields> ALL_BUCKET_FIELDS =
-      Opts.from(UnifiedOpts.fields(ImmutableSet.copyOf(BucketField.values())));
+      // todo: b/308194853
+      Opts.from(
+          UnifiedOpts.fields(
+              Arrays.stream(BucketField.values())
+                  .filter(f -> !f.equals(BucketField.OBJECT_RETENTION))
+                  .collect(ImmutableSet.toImmutableSet())));
 
   final StorageClient storageClient;
   final WriterFactory writerFactory;
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 8ba271c9..4bbf08d4 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
@@ -53,6 +53,7 @@ import com.google.cloud.storage.Acl.RawEntity;
 import com.google.cloud.storage.Acl.Role;
 import com.google.cloud.storage.Acl.User;
 import com.google.cloud.storage.BlobInfo.CustomerEncryption;
+import com.google.cloud.storage.BlobInfo.Retention;
 import com.google.cloud.storage.BucketInfo.Autoclass;
 import com.google.cloud.storage.BucketInfo.CustomPlacementConfig;
 import com.google.cloud.storage.BucketInfo.IamConfiguration;
@@ -63,6 +64,7 @@ import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleAction;
 import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleCondition;
 import com.google.cloud.storage.BucketInfo.LifecycleRule.SetStorageClassLifecycleAction;
 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.Conversions.Codec;
 import com.google.cloud.storage.Cors.Origin;
@@ -114,6 +116,9 @@ final class JsonConversions {
       Codec.of(this::iamConfigEncode, this::iamConfigDecode);
   private final Codec<Autoclass, Bucket.Autoclass> autoclassCodec =
       Codec.of(this::autoclassEncode, this::autoclassDecode);
+
+  private final Codec<ObjectRetention, Bucket.ObjectRetention> objectRetentionCodec =
+      Codec.of(this::objectRetentionEncode, this::objectRetentionDecode);
   private final Codec<LifecycleRule, Rule> lifecycleRuleCodec =
       Codec.of(this::lifecycleRuleEncode, this::lifecycleRuleDecode);
   private final Codec<LifecycleCondition, Condition> lifecycleConditionCodec =
@@ -124,6 +129,9 @@ final class JsonConversions {
   private final Codec<CustomerEncryption, StorageObject.CustomerEncryption>
       customerEncryptionCodec =
           Codec.of(this::customerEncryptionEncode, this::customerEncryptionDecode);
+
+  private final Codec<Retention, StorageObject.Retention> retentionCodec =
+      Codec.of(this::retentionEncode, this::retentionDecode);
   private final Codec<BlobId, StorageObject> blobIdCodec =
       Codec.of(this::blobIdEncode, this::blobIdDecode);
   private final Codec<BlobInfo, StorageObject> blobInfoCodec =
@@ -238,6 +246,19 @@ final class JsonConversions {
         from.getRetentionExpirationTimeOffsetDateTime(),
         dateTimeCodec::encode,
         to::setRetentionExpirationTime);
+
+    // 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
+    // bench.
+    // We should clean this up once the test bench can handle the retention field.
+    // See also the comment in StorageImpl.update(BlobInfo blobInfo, BlobTargetOption... options)
+    // todo: b/308194853
+    if (from.getModifiedFields().contains(Storage.BlobField.RETENTION)
+        && from.getRetention() == null) {
+      to.setRetention(Data.nullOf(StorageObject.Retention.class));
+    }
+    ifNonNull(from.getRetention(), this::retentionEncode, to::setRetention);
     to.setKmsKeyName(from.getKmsKeyName());
     to.setEventBasedHold(from.getEventBasedHold());
     to.setTemporaryHold(from.getTemporaryHold());
@@ -306,6 +327,7 @@ final class JsonConversions {
         from.getRetentionExpirationTime(),
         dateTimeCodec::decode,
         to::setRetentionExpirationTimeOffsetDateTime);
+    ifNonNull(from.getRetention(), this::retentionDecode, to::setRetention);
     return to.build();
   }
 
@@ -331,6 +353,20 @@ final class JsonConversions {
     return new CustomerEncryption(from.getEncryptionAlgorithm(), from.getKeySha256());
   }
 
+  private StorageObject.Retention retentionEncode(Retention from) {
+    StorageObject.Retention to = new StorageObject.Retention();
+    ifNonNull(from.getMode(), Retention.Mode::toString, to::setMode);
+    ifNonNull(from.getRetainUntilTime(), dateTimeCodec::encode, to::setRetainUntilTime);
+    return to;
+  }
+
+  private Retention retentionDecode(StorageObject.Retention from) {
+    Retention.Builder to = Retention.newBuilder();
+    ifNonNull(from.getMode(), Retention.Mode::valueOf, to::setMode);
+    ifNonNull(from.getRetainUntilTime(), dateTimeCodec::decode, to::setRetainUntilTime);
+    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));
@@ -400,6 +436,7 @@ final class JsonConversions {
         from.getCustomPlacementConfig(),
         this::customPlacementConfigEncode,
         to::setCustomPlacementConfig);
+    ifNonNull(from.getObjectRetention(), this::objectRetentionEncode, to::setObjectRetention);
     return to;
   }
 
@@ -450,7 +487,7 @@ final class JsonConversions {
         from.getCustomPlacementConfig(),
         this::customPlacementConfigDecode,
         to::setCustomPlacementConfig);
-
+    ifNonNull(from.getObjectRetention(), this::objectRetentionDecode, to::setObjectRetention);
     return to.build();
   }
 
@@ -504,6 +541,18 @@ final class JsonConversions {
     return to.build();
   }
 
+  private Bucket.ObjectRetention objectRetentionEncode(ObjectRetention from) {
+    Bucket.ObjectRetention to = new Bucket.ObjectRetention();
+    ifNonNull(from.getMode(), ObjectRetention.Mode::toString, to::setMode);
+    return to;
+  }
+
+  private ObjectRetention objectRetentionDecode(Bucket.ObjectRetention from) {
+    ObjectRetention.Builder to = ObjectRetention.newBuilder();
+    ifNonNull(from.getMode(), ObjectRetention.Mode::valueOf, to::setMode);
+    return to.build();
+  }
+
   private UniformBucketLevelAccess ublaEncode(IamConfiguration from) {
     UniformBucketLevelAccess to = new UniformBucketLevelAccess();
     to.setEnabled(from.isUniformBucketLevelAccessEnabled());
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 abec9c2f..5ca31c04 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
@@ -158,7 +158,9 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     CUSTOM_PLACEMENT_CONFIG("customPlacementConfig", "custom_placement_config"),
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
-    AUTOCLASS("autoclass");
+    AUTOCLASS("autoclass"),
+    @TransportCompatibility({Transport.HTTP})
+    OBJECT_RETENTION("objectRetention");
 
     static final List<BucketField> REQUIRED_FIELDS = ImmutableList.of(NAME);
 
@@ -256,7 +258,9 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     TIME_STORAGE_CLASS_UPDATED("timeStorageClassUpdated", "update_storage_class_time"),
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
-    CUSTOMER_ENCRYPTION("customerEncryption", "customer_encryption");
+    CUSTOMER_ENCRYPTION("customerEncryption", "customer_encryption"),
+    @TransportCompatibility({Transport.HTTP})
+    RETENTION("retention");
 
     static final List<NamedField> REQUIRED_FIELDS = ImmutableList.of(BUCKET, NAME);
 
@@ -324,6 +328,16 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BucketTargetOption(UnifiedOpts.predefinedDefaultObjectAcl(acl));
     }
 
+    /**
+     * Returns an option for enabling Object Retention on this bucket. Enabling this will create an
+     * ObjectRetention object in the created bucket (You must use this option, creating your own
+     * ObjectRetention object in the request won't work).
+     */
+    @TransportCompatibility({Transport.HTTP})
+    public static BucketTargetOption enableObjectRetention(boolean enable) {
+      return new BucketTargetOption(UnifiedOpts.enableObjectRetention(enable));
+    }
+
     /**
      * Returns an option for bucket's metageneration match. If this option is used the request will
      * fail if metageneration does not match.
@@ -1025,6 +1039,16 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobTargetOption(UnifiedOpts.kmsKeyName(kmsKeyName));
     }
 
+    /**
+     * Returns an option for overriding an Unlocked Retention policy. This must be set to true in
+     * order to change a policy from Unlocked to Locked, to set it to null, or to reduce its
+     * retainUntilTime attribute.
+     */
+    @TransportCompatibility({Transport.HTTP})
+    public static BlobTargetOption overrideUnlockedRetention(boolean overrideUnlockedRetention) {
+      return new BlobTargetOption(UnifiedOpts.overrideUnlockedRetention(overrideUnlockedRetention));
+    }
+
     /**
      * Deduplicate any options which are the same parameter. The value which comes last in {@code
      * os} will be the value included in the return.
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 48167b5d..1540f452 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
@@ -487,7 +487,17 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     Opts<ObjectTargetOpt> opts = Opts.unwrap(options).resolveFrom(blobInfo);
     Map<StorageRpc.Option, ?> optionsMap = opts.getRpcOptions();
     boolean unmodifiedBeforeOpts = blobInfo.getModifiedFields().isEmpty();
-    BlobInfo updated = opts.blobInfoMapper().apply(blobInfo.toBuilder()).build();
+    BlobInfo.Builder builder = blobInfo.toBuilder();
+
+    // This is a workaround until everything is in prod for both json and grpc.
+    // We need to make sure that the retention field is only included in the
+    // request if it was modified, so that we don't send a null object in a
+    // grpc or json request.
+    // todo: b/308194853
+    if (blobInfo.getModifiedFields().contains(BlobField.RETENTION)) {
+      builder.setRetention(blobInfo.getRetention());
+    }
+    BlobInfo updated = opts.blobInfoMapper().apply(builder).build();
     boolean unmodifiedAfterOpts = updated.getModifiedFields().isEmpty();
     if (unmodifiedBeforeOpts && unmodifiedAfterOpts) {
       return internalGetBlob(blobInfo.getBlobId(), optionsMap);
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 99a05342..7765596b 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
@@ -450,6 +450,14 @@ final class UnifiedOpts {
     return new PredefinedDefaultObjectAcl(predefinedAcl.getEntry());
   }
 
+  static EnableObjectRetention enableObjectRetention(boolean enable) {
+    return new EnableObjectRetention(enable);
+  }
+
+  static OverrideUnlockedRetention overrideUnlockedRetention(boolean overrideUnlockedRetention) {
+    return new OverrideUnlockedRetention(overrideUnlockedRetention);
+  }
+
   static Prefix prefix(@NonNull String prefix) {
     requireNonNull(prefix, "prefix must be non null");
     return new Prefix(prefix);
@@ -1399,6 +1407,20 @@ final class UnifiedOpts {
     }
   }
 
+  static final class EnableObjectRetention extends RpcOptVal<Boolean> implements BucketTargetOpt {
+    private static final long serialVersionUID = -2581147719605551578L;
+
+    private EnableObjectRetention(boolean val) {
+      super(StorageRpc.Option.ENABLE_OBJECT_RETENTION, val);
+    }
+
+    @Override
+    public Mapper<UpdateBucketRequest.Builder> updateBucket() {
+      return CrossTransportUtils.throwHttpJsonOnly(
+          Storage.BucketTargetOption.class, "enableObjectRetention(boolean)");
+    }
+  }
+
   static final class Prefix extends RpcOptVal<String> implements BucketListOpt, ObjectListOpt {
     private static final long serialVersionUID = -3973478772547687371L;
 
@@ -1623,6 +1645,22 @@ final class UnifiedOpts {
     }
   }
 
+  static final class OverrideUnlockedRetention extends RpcOptVal<Boolean>
+      implements ObjectTargetOpt {
+
+    private static final long serialVersionUID = -7764590745622588287L;
+
+    private OverrideUnlockedRetention(boolean val) {
+      super(StorageRpc.Option.OVERRIDE_UNLOCKED_RETENTION, val);
+    }
+
+    @Override
+    public Mapper<UpdateObjectRequest.Builder> updateObject() {
+      return CrossTransportUtils.throwHttpJsonOnly(
+          Storage.BlobTargetOption.class, "overrideUnlockedRetention(boolean)");
+    }
+  }
+
   static final class ShowDeletedKeys extends RpcOptVal<@NonNull Boolean> implements HmacKeyListOpt {
     private static final long serialVersionUID = -6604176744362903487L;
 
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 1402a4b5..04847ff0 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
@@ -364,6 +364,7 @@ public class HttpStorageRpc implements StorageRpc {
           .setPredefinedAcl(Option.PREDEFINED_ACL.getString(options))
           .setPredefinedDefaultObjectAcl(Option.PREDEFINED_DEFAULT_OBJECT_ACL.getString(options))
           .setUserProject(Option.USER_PROJECT.getString(options))
+          .setEnableObjectRetention(Option.ENABLE_OBJECT_RETENTION.getBoolean(options))
           .execute();
     } catch (IOException ex) {
       span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
@@ -622,6 +623,7 @@ public class HttpStorageRpc implements StorageRpc {
         .setIfMetagenerationNotMatch(Option.IF_METAGENERATION_NOT_MATCH.getLong(options))
         .setIfGenerationMatch(Option.IF_GENERATION_MATCH.getLong(options))
         .setIfGenerationNotMatch(Option.IF_GENERATION_NOT_MATCH.getLong(options))
+        .setOverrideUnlockedRetention(Option.OVERRIDE_UNLOCKED_RETENTION.getBoolean(options))
         .setUserProject(Option.USER_PROJECT.getString(options));
   }
 
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 bbfab105..7d671dee 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
@@ -71,7 +71,9 @@ public interface StorageRpc extends ServiceRpc {
     SHOW_DELETED_KEYS("showDeletedKeys"),
     REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"),
     DETECT_CONTENT_TYPE("detectContentType"),
-    RETURN_RAW_INPUT_STREAM("returnRawInputStream");
+    ENABLE_OBJECT_RETENTION("enableObjectRetention"),
+    RETURN_RAW_INPUT_STREAM("returnRawInputStream"),
+    OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention");
 
     private final String value;
 
diff --git a/pom.xml b/pom.xml
index 4a3310c9..f69a0f4f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,7 +86,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.125.11</version>
+        <version>1.125.12</version>
         <scope>test</scope>
       </dependency>
       <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 2b33c199..b484fd5a 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.11</version>
+      <version>1.125.12</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
@@ -72,7 +72,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>build-helper-maven-plugin</artifactId>
-        <version>3.4.0</version>
+        <version>3.5.0</version>
         <executions>
           <execution>
             <id>add-snippets-source</id>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 73f276bd..36543285 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.11</version>
+      <version>1.125.12</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index affa3256..f7ac5150 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -52,7 +52,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.11</version>
+      <version>1.125.12</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
@@ -63,7 +63,7 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>build-helper-maven-plugin</artifactId>
-        <version>3.4.0</version>
+        <version>3.5.0</version>
         <executions>
           <execution>
             <id>add-snippets-source</id>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 9f3344af..05efc112 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -72,7 +72,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.125.11</version>
+      <version>1.125.12</version>
       <scope>test</scope>
     </dependency>
   </dependencies>


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
index fb14f711..27e925f8 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
@@ -304,4 +304,15 @@ public final class TestUtils {
     }
     return false;
   }
+
+  /**
+   * GRPC test methods will sometimes use all bucket fields, which can cause an error if any aren't
+   * supported by GRPC. todo: b/308194853
+   */
+  public static Storage.BucketField[] filterOutHttpOnlyBucketFields(Storage.BucketField[] fields) {
+    return Arrays.stream(fields)
+        .filter(f -> !f.equals(Storage.BucketField.OBJECT_RETENTION))
+        .collect(ImmutableSet.toImmutableSet())
+        .toArray(new Storage.BucketField[0]);
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
index ab09709f..2c582668 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
@@ -45,6 +45,7 @@ import com.google.cloud.storage.Storage.BucketField;
 import com.google.cloud.storage.Storage.BucketGetOption;
 import com.google.cloud.storage.Storage.BucketTargetOption;
 import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
 import com.google.cloud.storage.it.runner.annotations.Backend;
@@ -150,7 +151,10 @@ public class ITAccessTest {
       assertThat(actual.getEtag()).isNotEmpty();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when updates happen, drop before our comparison
@@ -203,7 +207,10 @@ public class ITAccessTest {
       assertThat(actual.getEtag()).isNotEmpty();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when updates happen, drop before our comparison
@@ -255,7 +262,10 @@ public class ITAccessTest {
       assertThat(actual).isTrue();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when deletes happen, drop before our comparison
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 96b478d3..c2594a48 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
@@ -198,7 +198,10 @@ public final class ITBlobReadMaskTest {
               new Args<>(BlobField.TIME_CREATED, LazyAssertion.equal()),
               new Args<>(BlobField.TIME_DELETED, LazyAssertion.equal()),
               new Args<>(BlobField.TIME_STORAGE_CLASS_UPDATED, LazyAssertion.equal()),
-              new Args<>(BlobField.UPDATED, LazyAssertion.equal()));
+              new Args<>(BlobField.UPDATED, LazyAssertion.equal()),
+              new Args<>(
+                  BlobField.RETENTION,
+                  LazyAssertion.skip("TODO: jesse fill in buganizer bug here")));
       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/ITBucketAclTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketAclTest.java
index a1545202..1b05a0a1 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketAclTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketAclTest.java
@@ -34,6 +34,7 @@ import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BucketField;
 import com.google.cloud.storage.Storage.BucketGetOption;
 import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
 import com.google.cloud.storage.it.runner.annotations.Backend;
@@ -127,7 +128,10 @@ public final class ITBucketAclTest {
       assertThat(actual.getEtag()).isNotEmpty();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when updates happen, drop before our comparison
@@ -177,7 +181,10 @@ public final class ITBucketAclTest {
       assertThat(actual.getEtag()).isNotEmpty();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when updates happen, drop before our comparison
@@ -220,7 +227,11 @@ public final class ITBucketAclTest {
       assertThat(actual.getRole()).isEqualTo(readAll.getRole());
       assertThat(actual.getEtag()).isNotEmpty();
 
-      Bucket updated = storage.get(mgen1.getName(), BucketGetOption.fields(BucketField.values()));
+      Bucket updated =
+          storage.get(
+              mgen1.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(updated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when updates happen, drop before our comparison
@@ -254,7 +265,10 @@ public final class ITBucketAclTest {
       assertThat(actual).isTrue();
 
       Bucket bucketUpdated =
-          storage.get(bucket.getName(), BucketGetOption.fields(BucketField.values()));
+          storage.get(
+              bucket.getName(),
+              BucketGetOption.fields(
+                  TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
       assertThat(bucketUpdated.getMetageneration()).isNotEqualTo(bucket.getMetageneration());
 
       // etags change when deletes happen, drop before our comparison
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 d65c271d..93099d41 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
@@ -25,6 +25,7 @@ import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BucketField;
 import com.google.cloud.storage.Storage.BucketGetOption;
 import com.google.cloud.storage.Storage.BucketListOption;
+import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.ITBucketReadMaskTest.BucketReadMaskTestParameters;
 import com.google.cloud.storage.it.ReadMaskTestUtils.Args;
@@ -138,7 +139,10 @@ public final class ITBucketReadMaskTest {
           args.stream().map(Args::getField).map(Enum::name).sorted().collect(Collectors.toList());
 
       List<String> definedFields =
-          Arrays.stream(BucketField.values()).map(Enum::name).sorted().collect(Collectors.toList());
+          Arrays.stream(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values()))
+              .map(Enum::name)
+              .sorted()
+              .collect(Collectors.toList());
 
       assertThat(argsDefined).containsExactlyElementsIn(definedFields);
       return args;
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 f5b2b00c..e32be5c3 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
@@ -33,6 +33,7 @@ import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.BucketInfo.Autoclass;
 import com.google.cloud.storage.BucketInfo.CustomPlacementConfig;
+import com.google.cloud.storage.BucketInfo.ObjectRetention.Mode;
 import com.google.cloud.storage.Cors;
 import com.google.cloud.storage.HttpMethod;
 import com.google.cloud.storage.Rpo;
@@ -43,6 +44,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.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
 import com.google.cloud.storage.it.runner.annotations.Backend;
@@ -55,6 +57,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.time.Duration;
 import java.time.OffsetDateTime;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -111,7 +114,10 @@ public class ITBucketTest {
   @Test
   public void testGetBucketAllSelectedFields() {
     Bucket remoteBucket =
-        storage.get(bucket.getName(), Storage.BucketGetOption.fields(BucketField.values()));
+        storage.get(
+            bucket.getName(),
+            Storage.BucketGetOption.fields(
+                TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
     assertEquals(bucket.getName(), remoteBucket.getName());
     assertNotNull(remoteBucket.getCreateTime());
   }
@@ -420,6 +426,69 @@ public class ITBucketTest {
   }
 
   @Test
+  @CrossRun.Exclude(transports = Transport.GRPC)
+  public void testObjectRetention() {
+    String bucketName = generator.randomBucketName();
+
+    // Create a bucket with object retention enabled
+    storage.create(
+        BucketInfo.newBuilder(bucketName).build(), BucketTargetOption.enableObjectRetention(true));
+
+    try {
+      Bucket remoteBucket = storage.get(bucketName);
+      assertNotNull(remoteBucket.getObjectRetention());
+      assertEquals(Mode.ENABLED, remoteBucket.getObjectRetention().getMode());
+
+      OffsetDateTime now = OffsetDateTime.now();
+
+      // Create an object with a retention policy configured
+      storage.create(
+          BlobInfo.newBuilder(bucketName, "retentionObject")
+              .setRetention(
+                  BlobInfo.Retention.newBuilder()
+                      .setMode(BlobInfo.Retention.Mode.UNLOCKED)
+                      .setRetainUntilTime(now.plusDays(2))
+                      .build())
+              .build());
+
+      Blob remoteBlob = storage.get(bucketName, "retentionObject");
+      assertNotNull(remoteBlob.getRetention());
+      assertEquals(BlobInfo.Retention.Mode.UNLOCKED, remoteBlob.getRetention().getMode());
+
+      // Reduce the retainUntilTime of an object's retention policy
+      remoteBlob
+          .toBuilder()
+          .setRetention(
+              BlobInfo.Retention.newBuilder()
+                  .setMode(BlobInfo.Retention.Mode.UNLOCKED)
+                  .setRetainUntilTime(now.plusHours(1))
+                  .build())
+          .build()
+          .update(Storage.BlobTargetOption.overrideUnlockedRetention(true));
+
+      remoteBlob = storage.get(bucketName, "retentionObject");
+      assertEquals(
+          now.plusHours(1).toInstant().truncatedTo(ChronoUnit.SECONDS),
+          remoteBlob
+              .getRetention()
+              .getRetainUntilTime()
+              .toInstant()
+              .truncatedTo(ChronoUnit.SECONDS));
+
+      // Remove an unlocked retention policy
+      remoteBlob
+          .toBuilder()
+          .setRetention(null)
+          .build()
+          .update(Storage.BlobTargetOption.overrideUnlockedRetention(true));
+
+      remoteBlob = storage.get(bucketName, "retentionObject");
+      assertNull(remoteBlob.getRetention());
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
   public void testCreateBucketWithAutoclass_ARCHIVE() throws Exception {
     String bucketName = generator.randomBucketName();
     Autoclass autoclass =
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 dc99678c..4dbe55ad 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
@@ -50,6 +50,7 @@ import com.google.cloud.storage.Storage.PredefinedAcl;
 import com.google.cloud.storage.Storage.UpdateHmacKeyOption;
 import com.google.cloud.storage.StorageClass;
 import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
 import com.google.cloud.storage.it.runner.annotations.Backend;
@@ -326,7 +327,9 @@ public final class ITOptionRegressionTest {
             "updated",
             "versioning",
             "website");
-    s.get(b.getName(), BucketGetOption.fields(BucketField.values()));
+    s.get(
+        b.getName(),
+        BucketGetOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }
 
@@ -734,7 +737,8 @@ public final class ITOptionRegressionTest {
             "timeCreated",
             "timeDeleted",
             "timeStorageClassUpdated",
-            "updated");
+            "updated",
+            "retention");
     s.get(o.getBlobId(), BlobGetOption.fields(BlobField.values()));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }
@@ -813,7 +817,7 @@ public final class ITOptionRegressionTest {
             "items/updated",
             "items/versioning",
             "items/website");
-    s.list(BucketListOption.fields(BucketField.values()));
+    s.list(BucketListOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }
 
@@ -908,7 +912,8 @@ public final class ITOptionRegressionTest {
             "items/timeCreated",
             "items/timeDeleted",
             "items/timeStorageClassUpdated",
-            "items/updated");
+            "items/updated",
+            "items/retention");
     s.list(b.getName(), BlobListOption.fields(BlobField.values()));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());
   }