Skip to content

Firestore: Snapshot drops field from set() when followed by update() with pending writes #15864

@lukyanov

Description

@lukyanov

Description

When a document is created with setData() and then updateData() is called on the same document before the server confirms the setData(), snapshot listeners drop a field from the optimistic state reconstruction.

Expected: all fields from the original setData() should be present in the snapshot, merged with any pending updateData() mutations.

Actual: a plain string field is completely absent from the snapshot data — not even present in the dictionary keys — even though:

  • The field was confirmed present in the data passed to setData() (logged immediately before the call)
  • A direct getDocument(source: .server) confirms the server has the field
  • No updateData() call ever touches or deletes that field
  • Other plain fields from the same setData() call are preserved correctly

Diagnostic metadata on the broken snapshot:

  • document.metadata.hasPendingWrites = true
  • document.metadata.isFromCache = false

This was discovered via Flutter/FlutterFire (see firebase/flutterfire#18027).

Reproducing the issue

Native Swift reproduction:

import FirebaseFirestore

let db = Firestore.firestore()
let docRef = db.collection("test").document()

// 1. Create document with setData() — do NOT await completion (offline-first pattern)
docRef.setData([
    "ownerId": "user_123",        // plain String
    "title": "Test item",         // plain String
    "createdAt": FieldValue.serverTimestamp(),  // transform
    "status": 0                   // plain Int
])

// 2. Call updateData() before the server confirms the setData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    docRef.updateData([
        "retryCount": 1,
        "errorMessage": FieldValue.delete()
    ])
}

// 3. A snapshot listener on a query covering this document
//    returns the doc with 'ownerId' missing from the data dictionary.
db.collection("test")
    .order(by: "createdAt", descending: true)
    .addSnapshotListener { snapshot, error in
        guard let documents = snapshot?.documents else { return }
        for doc in documents {
            let data = doc.data()
            print("ownerId: \(data["ownerId"] ?? "nil")")    // Expected: "user_123", Actual: nil
            print("title: \(data["title"] ?? "nil")")        // "Test item" ✓
            print("status: \(data["status"] ?? "nil")")      // 0 ✓
            print("keys: \(Array(data.keys))")               // "ownerId" missing from keys

            // Direct server fetch confirms the field exists
            doc.reference.getDocument(source: .server) { serverDoc, _ in
                if let serverData = serverDoc?.data() {
                    print("server ownerId: \(serverData["ownerId"] ?? "nil")")  // "user_123" ✓
                }
            }
        }
    }

Conditions that trigger the bug:

  • setData() must include FieldValue.serverTimestamp() fields
  • updateData() must be called before the server confirms the setData()
  • The snapshot listener must use default source (not cache-only)
  • Multiple updateData() calls increase the likelihood of the field being dropped

Firebase SDK Version

12.8.0

Xcode Version

26.2

Installation Method

CocoaPods

Firebase Product(s)

Firestore

Targeted Platforms

iOS

Relevant Log Output

# Observed via Flutter diagnostic logging (flutterfire is a pass-through to the native SDK):

# 1. setData() data includes ownerId
[DIAG] ownerId=user_123, data={ownerId: user_123, title: Test item, createdAt: FieldValue(...), status: 0}

# 2. Snapshot listener: ownerId not in keys
[DIAG] ownerId missing, hasPendingWrites=true, isFromCache=false, keys=[title, retryCount, createdAt, status]

# 3. Direct server fetch: ownerId present
[DIAG] server doc: exists=true, ownerId=user_123, keys=[ownerId, title, retryCount, createdAt, status]

If using Swift Package Manager, the project's Package.resolved

N/A — using CocoaPods (via Flutter).

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
  - Firebase/Firestore (12.8.0):
    - Firebase/CoreOnly
    - FirebaseFirestore (~> 12.8.0)
  - FirebaseFirestore (12.8.0):
    - FirebaseFirestoreBinary (= 12.8.0)
  - FirebaseFirestoreAbseilBinary (1.2024072200.0)
  - FirebaseFirestoreBinary (12.8.0):
    - FirebaseCore (= 12.8.0)
    - FirebaseCoreExtension (= 12.8.0)
    - FirebaseFirestoreInternalBinary (= 12.8.0)
    - FirebaseSharedSwift (= 12.8.0)
  - FirebaseFirestoreGRPCBoringSSLBinary (1.69.0)
  - FirebaseFirestoreGRPCCoreBinary (1.69.0)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions