diff --git a/databricks-sdk-java/pom.xml b/databricks-sdk-java/pom.xml
index 5b612a3a4..beacf47b5 100644
--- a/databricks-sdk-java/pom.xml
+++ b/databricks-sdk-java/pom.xml
@@ -123,5 +123,17 @@
1.10.4
provided
+
+
+ com.google.protobuf
+ protobuf-java
+ 3.24.2
+
+
+
+ com.google.protobuf
+ protobuf-java-util
+ 3.24.2
+
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/ProtobufModule.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/ProtobufModule.java
new file mode 100644
index 000000000..c28e095e5
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/ProtobufModule.java
@@ -0,0 +1,119 @@
+package com.databricks.sdk.core.utils;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.google.protobuf.Duration;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Durations;
+import com.google.protobuf.util.Timestamps;
+import java.io.IOException;
+
+/** Jackson module for serializing and deserializing Google Protocol Buffers types. */
+public class ProtobufModule extends SimpleModule {
+
+ public ProtobufModule() {
+ super("ProtobufModule");
+
+ // FieldMask serializers.
+ addSerializer(FieldMask.class, new FieldMaskSerializer());
+ addDeserializer(FieldMask.class, new FieldMaskDeserializer());
+
+ // Duration serializers.
+ addSerializer(Duration.class, new DurationSerializer());
+ addDeserializer(Duration.class, new DurationDeserializer());
+
+ // Timestamp serializers.
+ addSerializer(Timestamp.class, new TimestampSerializer());
+ addDeserializer(Timestamp.class, new TimestampDeserializer());
+ }
+
+ /** Serializes FieldMask using simple string joining to preserve original casing. */
+ public static class FieldMaskSerializer extends JsonSerializer {
+ @Override
+ public void serialize(FieldMask fieldMask, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ // Unlike the Google API, we preserve the original casing of the field paths.
+ gen.writeString(String.join(",", fieldMask.getPathsList()));
+ }
+ }
+
+ /** Deserializes FieldMask using simple string splitting to preserve original casing. */
+ public static class FieldMaskDeserializer extends JsonDeserializer {
+ @Override
+ public FieldMask deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ String pathsString = p.getValueAsString();
+ if (pathsString == null || pathsString.trim().isEmpty()) {
+ return FieldMask.getDefaultInstance();
+ }
+
+ // Unlike the Google API, we preserve the original casing of the field paths.
+ FieldMask.Builder builder = FieldMask.newBuilder();
+ String[] paths = pathsString.split(",");
+ for (String path : paths) {
+ String trimmedPath = path.trim();
+ if (!trimmedPath.isEmpty()) {
+ builder.addPaths(trimmedPath);
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ /** Serializes Duration using Google's built-in utility. */
+ public static class DurationSerializer extends JsonSerializer {
+ @Override
+ public void serialize(Duration duration, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ gen.writeString(Durations.toString(duration));
+ }
+ }
+
+ /** Deserializes Duration using Google's built-in utility. */
+ public static class DurationDeserializer extends JsonDeserializer {
+ @Override
+ public Duration deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ String durationString = p.getValueAsString();
+ if (durationString == null || durationString.trim().isEmpty()) {
+ return Duration.getDefaultInstance();
+ }
+
+ try {
+ return Durations.parse(durationString.trim());
+ } catch (Exception e) {
+ throw new IOException("Invalid duration format: " + durationString, e);
+ }
+ }
+ }
+
+ /** Serializes Timestamp using Google's built-in utility. */
+ public static class TimestampSerializer extends JsonSerializer {
+ @Override
+ public void serialize(Timestamp timestamp, JsonGenerator gen, SerializerProvider serializers)
+ throws IOException {
+ gen.writeString(Timestamps.toString(timestamp));
+ }
+ }
+
+ /** Deserializes Timestamp using Google's built-in utility. */
+ public static class TimestampDeserializer extends JsonDeserializer {
+ @Override
+ public Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ String timestampString = p.getValueAsString();
+ if (timestampString == null || timestampString.trim().isEmpty()) {
+ return Timestamp.getDefaultInstance();
+ }
+
+ try {
+ return Timestamps.parse(timestampString.trim());
+ } catch (Exception e) {
+ throw new IOException("Invalid timestamp format: " + timestampString, e);
+ }
+ }
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java
index 8fd484996..ce1a84a2c 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/SerDeUtils.java
@@ -14,6 +14,7 @@ public static ObjectMapper createMapper() {
mapper
.registerModule(new JavaTimeModule())
.registerModule(new GuavaModule())
+ .registerModule(new ProtobufModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/ProtobufModuleTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/ProtobufModuleTest.java
new file mode 100644
index 000000000..6a335535e
--- /dev/null
+++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/ProtobufModuleTest.java
@@ -0,0 +1,137 @@
+package com.databricks.sdk.core.utils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.databricks.sdk.core.ApiClient;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.Duration;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Timestamp;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.*;
+
+public class ProtobufModuleTest {
+
+ // Helper wrapper classes for individual protobuf types.
+ public static class FieldMaskWrapper {
+ @JsonProperty("mask")
+ public FieldMask mask;
+ }
+
+ public static class DurationWrapper {
+ @JsonProperty("duration")
+ public Duration duration;
+ }
+
+ public static class TimestampWrapper {
+ @JsonProperty("timestamp")
+ public Timestamp timestamp;
+ }
+
+ // FieldMask Parameterized Tests.
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "",
+ "user.name,user.email",
+ "profile.snake_case,profile.avatar",
+ "profile.displayName,profile.avatar,settings.theme",
+ "nested.deep.field",
+ "nested.deep.field.value",
+ "complex.nested.deep.path.with.multiple.levels"
+ })
+ public void testFieldMaskSerializationAndRoundtrip(String pathsString)
+ throws JsonProcessingException {
+ // Create original FieldMask.
+ FieldMask.Builder builder = FieldMask.newBuilder();
+ if (!pathsString.isEmpty()) {
+ for (String path : pathsString.split(",")) {
+ builder.addPaths(path.trim());
+ }
+ }
+ FieldMask original = builder.build();
+
+ // Test serialization.
+ FieldMaskWrapper wrapper = new FieldMaskWrapper();
+ wrapper.mask = original;
+
+ String json = new ApiClient().serialize(wrapper);
+ String expectedJson = "{\"mask\":\"" + pathsString + "\"}";
+ assertEquals(expectedJson, json);
+
+ // Test roundtrip (deserialize and verify).
+ ObjectMapper mapper = SerDeUtils.createMapper();
+ FieldMaskWrapper deserialized = mapper.readValue(json, FieldMaskWrapper.class);
+ assertEquals(original.getPathsList(), deserialized.mask.getPathsList());
+ }
+
+ // Duration Parameterized Tests.
+ static Stream durationTestCases() {
+ return Stream.of(
+ Arguments.of(0L, 0, "0s"),
+ Arguments.of(1L, 0, "1s"),
+ Arguments.of(30L, 0, "30s"),
+ Arguments.of(3661L, 0, "3661s"), // 1 hour 1 minute 1 second
+ Arguments.of(0L, 500_000_000, "0.500s"), // 0.5 seconds
+ Arguments.of(1L, 500_000_000, "1.500s"), // 1.5 seconds
+ Arguments.of(30L, 3, "30.000000003s") // 30 seconds + 3 nanoseconds
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("durationTestCases")
+ public void testDurationSerializationAndRoundtrip(
+ long seconds, int nanos, String expectedDurationString) throws JsonProcessingException {
+ Duration original = Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+
+ DurationWrapper wrapper = new DurationWrapper();
+ wrapper.duration = original;
+
+ // Test serialization.
+ String json = new ApiClient().serialize(wrapper);
+ String expectedJson = "{\"duration\":\"" + expectedDurationString + "\"}";
+ assertEquals(expectedJson, json);
+
+ // Test roundtrip (deserialize and verify).
+ ObjectMapper mapper = SerDeUtils.createMapper();
+ DurationWrapper deserialized = mapper.readValue(json, DurationWrapper.class);
+ assertEquals(original.getSeconds(), deserialized.duration.getSeconds());
+ assertEquals(original.getNanos(), deserialized.duration.getNanos());
+ }
+
+ // Timestamp Parameterized Tests.
+ static Stream timestampTestCases() {
+ return Stream.of(
+ Arguments.of(0L, 0, "1970-01-01T00:00:00Z"), // Unix epoch
+ Arguments.of(1717756800L, 0, "2024-06-07T10:40:00Z"), // Test timestamp
+ Arguments.of(1609459200L, 0, "2021-01-01T00:00:00Z"), // New Year 2021
+ Arguments.of(1577836800L, 0, "2020-01-01T00:00:00Z"), // New Year 2020
+ Arguments.of(1640995200L, 500_000_000, "2022-01-01T00:00:00.500Z"), // With nanoseconds
+ Arguments.of(253402300799L, 999_999_999, "9999-12-31T23:59:59.999999999Z") // Far future
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("timestampTestCases")
+ public void testTimestampSerializationAndRoundtrip(
+ long seconds, int nanos, String expectedTimestampString) throws JsonProcessingException {
+ Timestamp original = Timestamp.newBuilder().setSeconds(seconds).setNanos(nanos).build();
+
+ TimestampWrapper wrapper = new TimestampWrapper();
+ wrapper.timestamp = original;
+
+ // Test serialization.
+ String json = new ApiClient().serialize(wrapper);
+ String expectedJson = "{\"timestamp\":\"" + expectedTimestampString + "\"}";
+ assertEquals(expectedJson, json);
+
+ // Test roundtrip (deserialize and verify).
+ ObjectMapper mapper = SerDeUtils.createMapper();
+ TimestampWrapper deserialized = mapper.readValue(json, TimestampWrapper.class);
+ assertEquals(original.getSeconds(), deserialized.timestamp.getSeconds());
+ assertEquals(original.getNanos(), deserialized.timestamp.getNanos());
+ }
+}