diff --git a/parse/src/main/java/com/parse/ParseObject.java b/parse/src/main/java/com/parse/ParseObject.java
index f2b2c1b1a..080bdc089 100644
--- a/parse/src/main/java/com/parse/ParseObject.java
+++ b/parse/src/main/java/com/parse/ParseObject.java
@@ -1474,6 +1474,9 @@ public Date getCreatedAt() {
/**
* Returns a set view of the keys contained in this object. This does not include createdAt,
* updatedAt, authData, or objectId. It does include things like username and ACL.
+ *
+ *
Note that while the returned set is unmodifiable, it is in fact not thread-safe, and
+ * creating a copy is recommended before iterating over it.
*/
public Set keySet() {
synchronized (mutex) {
diff --git a/parse/src/main/java/com/parse/ParseTraverser.java b/parse/src/main/java/com/parse/ParseTraverser.java
index 63a34edea..08caa4ed8 100644
--- a/parse/src/main/java/com/parse/ParseTraverser.java
+++ b/parse/src/main/java/com/parse/ParseTraverser.java
@@ -8,10 +8,12 @@
*/
package com.parse;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -94,7 +96,14 @@ private void traverseInternal(
} else if (root instanceof ParseObject) {
if (traverseParseObjects) {
ParseObject object = (ParseObject) root;
- for (String key : object.keySet()) {
+ // Because the object's keySet is not thread safe, because the underlying Map isn't,
+ // we need to create a copy before iterating over the object's keys to avoid
+ // ConcurrentModificationExceptions
+ Set keySet;
+ synchronized (object.mutex) {
+ keySet = new HashSet<>(object.keySet());
+ }
+ for (String key : keySet) {
traverseInternal(object.get(key), true, seen);
}
}