From 0ba19102b2f8ab1435b4df5b08bbe842903666eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Quenaudon?= Date: Thu, 2 Jul 2026 15:19:52 +0100 Subject: [PATCH] FieldMask: JSON serialisation part 2 --- .../squareup/wire/AnyMessageTypeAdapter.kt | 14 ++++++++ .../squareup/wire/WireTypeAdapterFactory.kt | 2 ++ .../squareup/wire/AnyMessageJsonAdapter.kt | 27 +++++++++++++++ .../squareup/wire/WireJsonAdapterFactory.kt | 2 ++ .../proto/proto3/contains_field_mask.proto | 2 ++ .../com/squareup/wire/WireJsonTest.kt | 34 +++++++++++++++---- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/wire-gson-support/src/main/java/com/squareup/wire/AnyMessageTypeAdapter.kt b/wire-gson-support/src/main/java/com/squareup/wire/AnyMessageTypeAdapter.kt index e9a1468e22..ae1c046938 100644 --- a/wire-gson-support/src/main/java/com/squareup/wire/AnyMessageTypeAdapter.kt +++ b/wire-gson-support/src/main/java/com/squareup/wire/AnyMessageTypeAdapter.kt @@ -43,6 +43,13 @@ class AnyMessageTypeAdapter( val protoAdapter = typeUrlToAdapter[value.typeUrl] ?: throw IOException("Cannot find type for url: ${value.typeUrl}") + if (protoAdapter == ProtoAdapter.FIELD_MASK) { + writer.name("value") + gson.getAdapter(FieldMask::class.java).write(writer, value.unpack(ProtoAdapter.FIELD_MASK)) + writer.endObject() + return + } + @Suppress("UNCHECKED_CAST") val delegate = gson.getAdapter(protoAdapter.type!!.java) as TypeAdapter> @@ -69,6 +76,13 @@ class AnyMessageTypeAdapter( val protoAdapter = typeUrlToAdapter[typeUrl] ?: throw IOException("Cannot resolve type: $typeUrl in ${reader.path}") + if (protoAdapter == ProtoAdapter.FIELD_MASK) { + val fieldMask = jsonElement.asJsonObject.get("value")?.let { + gson.getAdapter(FieldMask::class.java).fromJsonTree(it) + } ?: FieldMask() + return AnyMessage(typeUrl, ProtoAdapter.FIELD_MASK.encodeByteString(fieldMask)) + } + @Suppress("UNCHECKED_CAST") val delegate = gson.getAdapter(protoAdapter.type!!.java) as TypeAdapter> diff --git a/wire-gson-support/src/main/java/com/squareup/wire/WireTypeAdapterFactory.kt b/wire-gson-support/src/main/java/com/squareup/wire/WireTypeAdapterFactory.kt index c907bb85bf..6d4f2ecea7 100644 --- a/wire-gson-support/src/main/java/com/squareup/wire/WireTypeAdapterFactory.kt +++ b/wire-gson-support/src/main/java/com/squareup/wire/WireTypeAdapterFactory.kt @@ -20,6 +20,7 @@ import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import com.squareup.wire.internal.EnumJsonFormatter +import com.squareup.wire.internal.FieldMaskJsonFormatter import com.squareup.wire.internal.createRuntimeMessageAdapter /** @@ -82,6 +83,7 @@ class WireTypeAdapterFactory @JvmOverloads constructor( return when { rawType == AnyMessage::class.java -> AnyMessageTypeAdapter(gson, typeUrlToAdapter) as TypeAdapter + rawType == FieldMask::class.java -> GsonJsonIntegration.formatterAdapter(FieldMaskJsonFormatter) as TypeAdapter Message::class.java.isAssignableFrom(rawType) -> { val messageAdapter = createRuntimeMessageAdapter( messageType = rawType as Class, diff --git a/wire-moshi-adapter/src/main/java/com/squareup/wire/AnyMessageJsonAdapter.kt b/wire-moshi-adapter/src/main/java/com/squareup/wire/AnyMessageJsonAdapter.kt index 6005fc181e..5cb010d192 100644 --- a/wire-moshi-adapter/src/main/java/com/squareup/wire/AnyMessageJsonAdapter.kt +++ b/wire-moshi-adapter/src/main/java/com/squareup/wire/AnyMessageJsonAdapter.kt @@ -41,6 +41,15 @@ internal class AnyMessageJsonAdapter( val protoAdapter = typeUrlToAdapter[value.typeUrl] ?: throw JsonDataException("Cannot find type for url: ${value.typeUrl} in ${writer.path}") + if (protoAdapter == ProtoAdapter.FIELD_MASK) { + @Suppress("UNCHECKED_CAST") + val delegate = moshi.adapter(FieldMask::class.java) as JsonAdapter + writer.name("value") + delegate.toJson(writer, value.unpack(ProtoAdapter.FIELD_MASK)) + writer.endObject() + return + } + @Suppress("UNCHECKED_CAST") val delegate = moshi.adapter(protoAdapter.type!!.java) as JsonAdapter> @@ -63,6 +72,24 @@ internal class AnyMessageJsonAdapter( val protoAdapter = typeUrlToAdapter[typeUrl] ?: throw JsonDataException("Cannot resolve type: $typeUrl in ${reader.path}") + if (protoAdapter == ProtoAdapter.FIELD_MASK) { + @Suppress("UNCHECKED_CAST") + val delegate = moshi.adapter(FieldMask::class.java) as JsonAdapter + var fieldMask = FieldMask() + + reader.beginObject() + while (reader.hasNext()) { + when (reader.nextName()) { + "@type" -> reader.skipValue() + "value" -> fieldMask = delegate.fromJson(reader) ?: FieldMask() + else -> reader.skipValue() + } + } + reader.endObject() + + return AnyMessage(typeUrl, ProtoAdapter.FIELD_MASK.encodeByteString(fieldMask)) + } + @Suppress("UNCHECKED_CAST") val delegate = moshi.adapter(protoAdapter.type!!.java) as JsonAdapter> diff --git a/wire-moshi-adapter/src/main/java/com/squareup/wire/WireJsonAdapterFactory.kt b/wire-moshi-adapter/src/main/java/com/squareup/wire/WireJsonAdapterFactory.kt index 4d5c01f138..eeba9e23b0 100644 --- a/wire-moshi-adapter/src/main/java/com/squareup/wire/WireJsonAdapterFactory.kt +++ b/wire-moshi-adapter/src/main/java/com/squareup/wire/WireJsonAdapterFactory.kt @@ -19,6 +19,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.wire.internal.EnumJsonFormatter +import com.squareup.wire.internal.FieldMaskJsonFormatter import com.squareup.wire.internal.createRuntimeMessageAdapter import java.lang.reflect.Type @@ -83,6 +84,7 @@ class WireJsonAdapterFactory @JvmOverloads constructor( return when { annotations.isNotEmpty() -> null rawType == AnyMessage::class.java -> AnyMessageJsonAdapter(moshi, typeUrlToAdapter) + rawType == FieldMask::class.java -> MoshiJsonIntegration.formatterAdapter(FieldMaskJsonFormatter) Message::class.java.isAssignableFrom(rawType) -> { val messageAdapter = createRuntimeMessageAdapter( messageType = type as Class, diff --git a/wire-tests/fixtures/shared/proto/proto3/contains_field_mask.proto b/wire-tests/fixtures/shared/proto/proto3/contains_field_mask.proto index 77255e1c97..ef5b40c199 100644 --- a/wire-tests/fixtures/shared/proto/proto3/contains_field_mask.proto +++ b/wire-tests/fixtures/shared/proto/proto3/contains_field_mask.proto @@ -18,8 +18,10 @@ syntax = "proto3"; package squareup.proto3; import "google/protobuf/field_mask.proto"; +import "google/protobuf/any.proto"; message ContainsFieldMask { google.protobuf.FieldMask mask = 1; repeated google.protobuf.FieldMask masks = 2; + google.protobuf.Any any_mask = 3; } diff --git a/wire-tests/wire-json-shared-kotlin-tests/com/squareup/wire/WireJsonTest.kt b/wire-tests/wire-json-shared-kotlin-tests/com/squareup/wire/WireJsonTest.kt index c7fdffee22..8b0f78c401 100644 --- a/wire-tests/wire-json-shared-kotlin-tests/com/squareup/wire/WireJsonTest.kt +++ b/wire-tests/wire-json-shared-kotlin-tests/com/squareup/wire/WireJsonTest.kt @@ -269,6 +269,7 @@ class WireJsonTest { } @Test fun fieldMask() { + val anyFieldMask = FieldMask(listOf("masked_name")) val value = ContainsFieldMask.Builder() .mask(FieldMask(listOf("user.display_name", "photo", "foo_bar.baz_qux"))) .masks( @@ -277,11 +278,22 @@ class WireJsonTest { FieldMask(listOf("updated_at.seconds")), ), ) + .any_mask( + AnyMessage( + ProtoAdapter.FIELD_MASK.typeUrl!!, + ProtoAdapter.FIELD_MASK.encodeByteString(anyFieldMask), + ), + ) .build() + val anyFieldName = if (jsonLibrary.preservingProtoFieldNames) "any_mask" else "anyMask" val json = """ |{ | "mask": "user.displayName,photo,fooBar.bazQux", - | "masks": ["displayName", "updatedAt.seconds"] + | "masks": ["displayName", "updatedAt.seconds"], + | "$anyFieldName": { + | "@type": "type.googleapis.com/google.protobuf.FieldMask", + | "value": "maskedName" + | } |} """.trimMargin() @@ -295,6 +307,14 @@ class WireJsonTest { ) } + @Test fun rootFieldMask() { + val value = FieldMask(listOf("user.display_name", "photo", "foo_bar.baz_qux")) + val json = "\"user.displayName,photo,fooBar.bazQux\"" + + assertThat(jsonLibrary.toJson(value, FieldMask::class.java)).isEqualTo(json) + assertThat(jsonLibrary.fromJson(json, FieldMask::class.java)).isEqualTo(value) + } + @Test fun anyMessageWithUnregisteredTypeOnReading() { try { jsonLibrary.fromJson(PIZZA_DELIVERY_UNKNOWN_TYPE_JSON, PizzaDelivery::class.java) @@ -1223,7 +1243,7 @@ class WireJsonTest { private val moshi = Moshi.Builder() .add( WireJsonAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .build() @@ -1245,7 +1265,7 @@ class WireJsonTest { private val gson = GsonBuilder() .registerTypeAdapterFactory( WireTypeAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .disableHtmlEscaping() .create() @@ -1268,7 +1288,7 @@ class WireJsonTest { private val moshi = Moshi.Builder() .add( WireJsonAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .build() @@ -1290,7 +1310,7 @@ class WireJsonTest { private val gson = GsonBuilder() .registerTypeAdapterFactory( WireTypeAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .disableHtmlEscaping() .create() @@ -1313,7 +1333,7 @@ class WireJsonTest { private val moshi = Moshi.Builder() .add( WireJsonAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .build() @@ -1335,7 +1355,7 @@ class WireJsonTest { private val gson = GsonBuilder() .registerTypeAdapterFactory( WireTypeAdapterFactory(writeIdentityValues = writeIdentityValues, preservingProtoFieldNames = preservingProtoFieldNames) - .plus(listOf(BuyOneGetOnePromotion.ADAPTER)), + .plus(listOf(BuyOneGetOnePromotion.ADAPTER, ProtoAdapter.FIELD_MASK)), ) .disableHtmlEscaping() .create()