Skip to content

Commit baa0456

Browse files
authored
dataconnect: ignore unknown keys in response data instead of throwing an exception (#7314)
1 parent e35a424 commit baa0456

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

firebase-dataconnect/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
- [changed] Ignore unknown fields in response data instead of throwing a
4+
`DataConnectOperationException` with message "decoding data from the server's response failed: An
5+
unknown field for index -3"
6+
37
# 17.0.0
48

59
- [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher.

firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,10 @@ mutation createPersonWithPartialFailureInTransaction($id: String!, $name: String
105105
person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "te36b3zkvn")
106106
}
107107

108+
mutation create5People @auth(level: PUBLIC) {
109+
person1: person_upsert(data: { id: "8a218894521f457a9be45a0986494058", name: "Name1" })
110+
person2: person_upsert(data: { id: "464443371f284194be4b2e78c3ef000c", name: "Name2" })
111+
person3: person_upsert(data: { id: "903d83db81754bd29860458f127ef124", name: "Name3" })
112+
person4: person_upsert(data: { id: "ef8de2a4a6de400e94d555f148b643c0", name: "Name4" })
113+
person5: person_upsert(data: { id: "8584fd7ca2b6453da18d21d4341f1804", name: "Name5" })
114+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
@file:OptIn(ExperimentalFirebaseDataConnect::class)
18+
19+
package com.google.firebase.dataconnect
20+
21+
import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
22+
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
23+
import com.google.firebase.dataconnect.testutil.schemas.AllTypesSchema
24+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema
25+
import io.kotest.assertions.withClue
26+
import io.kotest.matchers.shouldBe
27+
import io.kotest.property.Arb
28+
import io.kotest.property.arbitrary.next
29+
import kotlinx.coroutines.test.runTest
30+
import kotlinx.serialization.Serializable
31+
import kotlinx.serialization.serializer
32+
import org.junit.Test
33+
34+
class UnknownKeysIntegrationTest : DataConnectIntegrationTestBase() {
35+
36+
private val personSchema by lazy { PersonSchema(dataConnectFactory) }
37+
private val allTypesSchema by lazy { AllTypesSchema(dataConnectFactory) }
38+
39+
@Test
40+
fun unknownKeysInQueryResponseDataShouldBeIgnored() = runTest {
41+
val id = Arb.dataConnect.uuid().next()
42+
allTypesSchema
43+
.createPrimitive(
44+
AllTypesSchema.PrimitiveData(
45+
id = id,
46+
idFieldNullable = "0d2fcdf1c4a84c64a87f3c7932b31749",
47+
intField = 42,
48+
intFieldNullable = 43,
49+
floatField = 123.45,
50+
floatFieldNullable = 678.91,
51+
booleanField = true,
52+
booleanFieldNullable = false,
53+
stringField = "TestString",
54+
stringFieldNullable = "TestNullableString"
55+
)
56+
)
57+
.execute()
58+
59+
@Serializable
60+
data class PrimitiveQueryDataValues(
61+
val intFieldNullable: Int?,
62+
val booleanField: Boolean,
63+
val booleanFieldNullable: Boolean?,
64+
val stringField: String,
65+
val stringFieldNullable: String?,
66+
)
67+
68+
/** An adaptation of [AllTypesSchema.GetPrimitiveQuery.Data] with some keys missing. */
69+
@Serializable
70+
data class PrimitiveQueryDataMissingSomeKeys(val primitive: PrimitiveQueryDataValues)
71+
72+
val result =
73+
allTypesSchema
74+
.getPrimitive(id = id)
75+
.withDataDeserializer(serializer<PrimitiveQueryDataMissingSomeKeys>())
76+
.execute()
77+
78+
result.data.primitive shouldBe
79+
PrimitiveQueryDataValues(
80+
intFieldNullable = 43,
81+
booleanField = true,
82+
booleanFieldNullable = false,
83+
stringField = "TestString",
84+
stringFieldNullable = "TestNullableString"
85+
)
86+
}
87+
88+
@Test
89+
fun unknownKeysInMutationResponseDataShouldBeIgnored() = runTest {
90+
@Serializable data class PersonKeyWithId(val id: String)
91+
val person1 = PersonKeyWithId(id = "8a218894521f457a9be45a0986494058")
92+
val person4 = PersonKeyWithId(id = "ef8de2a4a6de400e94d555f148b643c0")
93+
val mutationRef =
94+
personSchema.dataConnect.mutation("create5People", Unit, serializer<Unit>(), serializer())
95+
96+
// Precondition check: Verify that the response contains person1..person5 and the expected IDs.
97+
withClue("precondition check") {
98+
@Serializable
99+
data class Create5PeopleData(
100+
val person1: PersonKeyWithId,
101+
val person3: PersonKeyWithId,
102+
val person2: PersonKeyWithId,
103+
val person4: PersonKeyWithId,
104+
val person5: PersonKeyWithId,
105+
)
106+
107+
val mutationResult =
108+
mutationRef.withDataDeserializer(serializer<Create5PeopleData>()).execute()
109+
mutationResult.data shouldBe
110+
Create5PeopleData(
111+
person1 = person1,
112+
person2 = PersonKeyWithId(id = "464443371f284194be4b2e78c3ef000c"),
113+
person3 = PersonKeyWithId(id = "903d83db81754bd29860458f127ef124"),
114+
person4 = person4,
115+
person5 = PersonKeyWithId(id = "8584fd7ca2b6453da18d21d4341f1804"),
116+
)
117+
}
118+
119+
withClue("actual test") {
120+
// Create5PeopleDataWithMissingKeys is missing "person2.id", "person3", and "person5" which
121+
// will be present in the response.
122+
@Serializable data class PersonKeyWithoutId(val foo: Nothing? = null)
123+
@Serializable
124+
data class Create5PeopleDataWithMissingKeys(
125+
val person1: PersonKeyWithId,
126+
val person2: PersonKeyWithoutId,
127+
val person4: PersonKeyWithId,
128+
)
129+
130+
val mutationResult =
131+
mutationRef.withDataDeserializer(serializer<Create5PeopleDataWithMissingKeys>()).execute()
132+
mutationResult.data shouldBe
133+
Create5PeopleDataWithMissingKeys(
134+
person1 = person1,
135+
person2 = PersonKeyWithoutId(),
136+
person4 = PersonKeyWithId(id = "ef8de2a4a6de400e94d555f148b643c0"),
137+
)
138+
}
139+
}
140+
}

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/ProtoStructDecoder.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,12 @@ private class ProtoStructValueDecoder(
177177
addAll(struct.fieldsMap.keys)
178178
addAll(descriptor.elementNames)
179179
}
180-
elementIndexes = names.map(descriptor::getElementIndex).sorted().iterator()
180+
elementIndexes =
181+
names
182+
.map(descriptor::getElementIndex)
183+
.filter { it != CompositeDecoder.UNKNOWN_NAME } // ignore unknown keys
184+
.sorted()
185+
.iterator()
181186
}
182187

183188
return elementIndexes

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/ProtoStructDecoderUnitTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package com.google.firebase.dataconnect
1919

2020
import com.google.firebase.dataconnect.SerializationTestData.serializationTestDataAllTypes
2121
import com.google.firebase.dataconnect.SerializationTestData.withEmptyListOfUnitRecursive
22+
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
23+
import com.google.firebase.dataconnect.testutil.property.arbitrary.twoValues
2224
import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
2325
import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto
2426
import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct
@@ -477,6 +479,21 @@ class ProtoStructDecoderUnitTest {
477479
}
478480
}
479481

482+
@Test
483+
fun `decodeFromStruct() ignores unknown struct keys`() = runTest {
484+
@Serializable data class TestData1(val value1: String, val value2: String)
485+
@Serializable data class TestData2(val value1: String)
486+
487+
val testData1Arb: Arb<TestData1> =
488+
Arb.twoValues(Arb.dataConnect.string()).map { (value1, value2) -> TestData1(value1, value2) }
489+
490+
checkAll(propTestConfig, testData1Arb) { testData1 ->
491+
val struct = encodeToStruct(testData1)
492+
val decodedTestData = decodeFromStruct<TestData2>(struct)
493+
decodedTestData shouldBe TestData2(testData1.value1)
494+
}
495+
}
496+
480497
@Test
481498
fun `decodeFromStruct() should throw SerializationException if attempting to decode an Int`() {
482499
assertDecodeFromStructThrowsIncorrectKindCase<Int>(

0 commit comments

Comments
 (0)