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); } }