How to Edit Firestore Documents Without Breaking Your Data
Updated
Editing a Firestore document is easy. Editing it without changing something you didn't mean to is harder. The usual culprits are type conversions, write semantics, and concurrent edits. This guide covers each, with what to do in code and what to expect from a Firestore editor.
Firestore types that JSON can't represent
| Firestore type | What goes wrong in plain JSON |
|---|---|
| Timestamp | Becomes a string or {"_seconds": ..., "_nanoseconds": ...}; writing it back stores a string or a map, not a timestamp |
| Reference | Becomes a path string; writing it back stores a string, breaking joins |
| 64-bit integer | JavaScript numbers lose precision above 253, so IDs and counters get rounded |
Double with no fraction (e.g. 1.0) | Round-trips as the integer 1, changing its type |
| GeoPoint, Bytes | Become maps or base64 strings |
Your app may still work after these changes, until a query compares by type, a client SDK expects a Timestamp, or a rounded ID points at the wrong record. Edit with a tool that keeps types explicit. Firewatch shows them as tagged values, such as {"$timestamp": "2024-05-01T12:00:00Z"} and {"$ref": "users/alice"}.
set, update, and merge do different things
set(data)replaces the whole document. Fields you leave out are deleted.set(data, { merge: true })changes only the fields you pass. Fields you leave out are kept, so removing a key from your object does not delete it.update(data)changes only the given fields, and fails if the document doesn't exist.- To delete a single field, use
FieldValue.delete()in an update.
A common bug in homemade editors is using merge for saves: you delete a field in the editor, save, and it's still there. A full-document editor should replace the document, so what you see is exactly what gets stored.
Don't overwrite someone else's changes
If you open a document, a teammate or your app changes it, and then you save, a plain set silently undoes their change. In code, use a transaction or a precondition on the document's update time:
const snap = await ref.get();
// ...user edits newData...
// Unsafe: await ref.set(newData) (last write wins)
// Safe: only write if nothing changed since we read it
await db.runTransaction(async (tx) => {
const current = await tx.get(ref);
if (!current.updateTime.isEqual(snap.updateTime)) {
throw new Error("Document changed since you loaded it");
}
tx.set(ref, newData);
});Firewatch saves with an update-time precondition, so a stale save is refused and you're asked to reload.
Deleting safely
- Deleting a document does not delete its subcollections. Delete those separately if you mean to.
- There's no recycle bin. Enable point-in-time recovery or scheduled backups on production databases so you can recover from mistakes.
- For day-to-day work, an undo helps: Firewatch keeps the deleted documents in memory briefly and can restore them, unless something new has been written at the same path since.
A safe editing checklist
- Browse production in read-only mode until you need to write.
- Check field types before and after editing.
- Save full documents with a precondition; never blind-merge.
- Export the collection first before bulk changes (see exporting to JSON).
- Make sure your IAM role is no broader than you need (see Firestore IAM roles).