Add ProxySerializer API to provide EntityProxy persistence primitives.
Issue 5523.
http://code.google.com/p/google-web-toolkit/wiki/RequestFactory_2_1_1
Patch by: bobv
Review by: rchandia, cramsdale

Review at http://gwt-code-reviews.appspot.com/1148801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9294 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
index e8aa4bb..c7d227f 100644
--- a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
+++ b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
@@ -57,6 +57,14 @@
       return Collections.emptyList();
     }
 
+    public boolean isIndexed() {
+      return false;
+    }
+
+    public boolean isKeyed() {
+      return false;
+    }
+
     public boolean isNull(int index) {
       throw new UnsupportedOperationException();
     }
@@ -65,6 +73,10 @@
       throw new UnsupportedOperationException();
     }
 
+    public boolean isString() {
+      return true;
+    }
+
     public int size() {
       return 0;
     }
@@ -107,6 +119,14 @@
     return Collections.unmodifiableList(toReturn);
   }
 
+  public native boolean isIndexed() /*-{
+    return this instanceof Array;
+  }-*/;
+
+  public boolean isKeyed() {
+    return !isString() && !isIndexed();
+  }
+
   public native boolean isNull(int index) /*-{
     return this[index] == null;
   }-*/;
@@ -115,6 +135,10 @@
     return this[key] == null;
   }-*/;
 
+  public native boolean isString() /*-{
+    return typeof(this) == 'string' || this instanceof String;
+  }-*/;
+
   public native int size() /*-{
     return this.length;
   }-*/;
diff --git a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java
index b331ad5..9f0dc3f 100644
--- a/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java
+++ b/user/src/com/google/gwt/autobean/server/SimpleBeanHandler.java
@@ -15,8 +15,6 @@
  */
 package com.google.gwt.autobean.server;
 
-import com.google.gwt.autobean.shared.AutoBeanCodex;
-
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 
@@ -55,6 +53,6 @@
    */
   @Override
   public String toString() {
-    return AutoBeanCodex.encode(bean).getPayload();
+    return bean.getPropertyMap().toString();
   }
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
index 6550f82..6dd80e3 100644
--- a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
+++ b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
@@ -62,7 +62,6 @@
 
   private final JSONArray array;
   private final JSONObject obj;
-
   private final String string;
 
   private JsonSplittable(JSONArray array) {
@@ -125,6 +124,14 @@
     }
   }
 
+  public boolean isIndexed() {
+    return array != null;
+  }
+
+  public boolean isKeyed() {
+    return obj != null;
+  }
+
   public boolean isNull(int index) {
     return array.isNull(index);
   }
@@ -134,6 +141,10 @@
     return !obj.has(key) || obj.isNull(key);
   }
 
+  public boolean isString() {
+    return string != null;
+  }
+
   public int size() {
     return array.length();
   }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
index 129892d..ed1930f 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
@@ -17,22 +17,99 @@
 
 import com.google.gwt.core.client.impl.WeakMapping;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
  * Utility methods for working with AutoBeans.
  */
 public final class AutoBeanUtils {
+  /*
+   * TODO(bobv): Make Comparison a real type that holds a map contain the diff
+   * between the two objects. Then export a Map of PendingComparison to
+   * Comparisons as a public API to make it easy for developers to perform deep
+   * diffs across a graph structure.
+   * 
+   * Three-way merge...
+   */
+
+  private enum Comparison {
+    TRUE, FALSE, PENDING;
+  }
 
   /**
-   * Returns a map of properties that differ between two AutoBeans. The keys are
-   * property names and the values are the value of the property in
-   * <code>b</code>. Properties present in <code>a</code> but missing in
-   * <code>b</code> will be represented by <code>null</code> values. This
-   * implementation will compare AutoBeans of different parameterizations,
-   * although the diff produced is likely meaningless.
+   * A Pair where order does not matter and the objects are compared by
+   * identity.
+   */
+  private static class PendingComparison {
+    private final AutoBean<?> a;
+    private final AutoBean<?> b;
+    private final int hashCode;
+
+    public PendingComparison(AutoBean<?> a, AutoBean<?> b) {
+      this.a = a;
+      this.b = b;
+      // Don't make relatively prime since order does not matter
+      hashCode = System.identityHashCode(a) + System.identityHashCode(b);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof PendingComparison)) {
+        return false;
+      }
+      PendingComparison other = (PendingComparison) o;
+      return a == other.a && b == other.b || // Direct match
+          a == other.b && b == other.a; // Swapped
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+  }
+
+  /**
+   * Compare two graphs of AutoBeans based on values.
+   * <p>
+   * <ul>
+   * <li>AutoBeans are compared based on type and property values</li>
+   * <li>Lists are compared with element-order equality</li>
+   * <li>Sets and all other Collection types are compare with bag equality</li>
+   * <li>Maps are compared as a lists of keys-value pairs</li>
+   * <li>{@link Splittable Splittables} are compared by value</li>
+   * </ul>
+   * <p>
+   * This will work for both simple and wrapper AutoBeans.
+   * <p>
+   * This method may crawl the entire object graph reachable from the input
+   * parameters and may be arbitrarily expensive to compute.
+   * 
+   * @param a an {@link AutoBean}
+   * @param b an {@link AutoBean}
+   * @return {@code false} if any values in the graph reachable through
+   *         <code>a</code> are different from those reachable from
+   *         <code>b</code>
+   */
+  public static boolean deepEquals(AutoBean<?> a, AutoBean<?> b) {
+    return sameOrEquals(a, b, new HashMap<PendingComparison, Comparison>());
+  }
+
+  /**
+   * Returns a map of properties that differ (via {@link Object#equals(Object)})
+   * between two AutoBeans. The keys are property names and the values are the
+   * value of the property in <code>b</code>. Properties present in
+   * <code>a</code> but missing in <code>b</code> will be represented by
+   * <code>null</code> values. This implementation will compare AutoBeans of
+   * different parameterizations, although the diff produced is likely
+   * meaningless.
    * <p>
    * This will work for both simple and wrapper AutoBeans.
    * 
@@ -137,6 +214,251 @@
   }
 
   /**
+   * Compare two AutoBeans, this method has the type fan-out.
+   */
+  static boolean sameOrEquals(Object value, Object otherValue,
+      Map<PendingComparison, Comparison> pending) {
+    if (value == otherValue) {
+      // Fast exit
+      return true;
+    }
+
+    if (value instanceof Collection<?> && otherValue instanceof Collection<?>) {
+      // Check collections
+      return sameOrEquals((Collection<?>) value, (Collection<?>) otherValue,
+          pending, null);
+    }
+
+    if (value instanceof Map<?, ?> && otherValue instanceof Map<?, ?>) {
+      // Check maps
+      return sameOrEquals((Map<?, ?>) value, (Map<?, ?>) otherValue, pending);
+    }
+
+    if (value instanceof Splittable && otherValue instanceof Splittable) {
+      return sameOrEquals((Splittable) value, (Splittable) otherValue, pending);
+    }
+
+    // Possibly substitute the AutoBean for its shim
+    {
+      AutoBean<?> maybeValue = AutoBeanUtils.getAutoBean(value);
+      AutoBean<?> maybeOther = AutoBeanUtils.getAutoBean(otherValue);
+      if (maybeValue != null && maybeOther != null) {
+        value = maybeValue;
+        otherValue = maybeOther;
+      }
+    }
+
+    if (value instanceof AutoBean<?> && otherValue instanceof AutoBean<?>) {
+      // Check ValueProxies
+      return sameOrEquals((AutoBean<?>) value, (AutoBean<?>) otherValue,
+          pending);
+    }
+
+    if (value == null ^ otherValue == null) {
+      // One is null, the other isn't
+      return false;
+    }
+
+    if (value != null && !value.equals(otherValue)) {
+      // Regular object equality
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * If a comparison between two AutoBeans is currently pending, this method
+   * will skip their comparison.
+   */
+  private static boolean sameOrEquals(AutoBean<?> value,
+      AutoBean<?> otherValue, Map<PendingComparison, Comparison> pending) {
+    if (value == otherValue) {
+      // Simple case
+      return true;
+    } else if (!value.getType().equals(otherValue.getType())) {
+      // Beans of different types
+      return false;
+    }
+
+    /*
+     * The PendingComparison key allows us to break reference cycles when
+     * crawling the graph. Since the entire operation is essentially a
+     * concatenated && operation, it's ok to speculatively return true for
+     * repeated a.equals(b) tests.
+     */
+    PendingComparison key = new PendingComparison(value, otherValue);
+    Comparison previous = pending.get(key);
+    if (previous == null) {
+      // Prevent the same comparison from being made
+      pending.put(key, Comparison.PENDING);
+
+      // Compare each property
+      Map<String, Object> beanProperties = AutoBeanUtils.getAllProperties(value);
+      Map<String, Object> otherProperties = AutoBeanUtils.getAllProperties(otherValue);
+      for (Map.Entry<String, Object> entry : beanProperties.entrySet()) {
+        Object property = entry.getValue();
+        Object otherProperty = otherProperties.get(entry.getKey());
+        if (!sameOrEquals(property, otherProperty, pending)) {
+          pending.put(key, Comparison.FALSE);
+          return false;
+        }
+      }
+      pending.put(key, Comparison.TRUE);
+      return true;
+    } else {
+      // Return true for TRUE or PENDING
+      return !Comparison.FALSE.equals(previous);
+    }
+  }
+
+  /**
+   * Compare two collections by size, then by contents. List comparisons will
+   * preserve order. All other collections will be treated with bag semantics.
+   */
+  private static boolean sameOrEquals(Collection<?> collection,
+      Collection<?> otherCollection,
+      Map<PendingComparison, Comparison> pending, Map<Object, Object> pairs) {
+    if (collection.size() != otherCollection.size()
+        || !collection.getClass().equals(otherCollection.getClass())) {
+      return false;
+    }
+
+    if (collection instanceof List<?>) {
+      // Lists we can simply iterate over
+      Iterator<?> it = collection.iterator();
+      Iterator<?> otherIt = otherCollection.iterator();
+      while (it.hasNext()) {
+        assert otherIt.hasNext();
+        Object element = it.next();
+        Object otherElement = otherIt.next();
+        if (!sameOrEquals(element, otherElement, pending)) {
+          return false;
+        }
+        if (pairs != null) {
+          pairs.put(element, otherElement);
+        }
+      }
+    } else {
+      // Do an n*m comparison on any other collection type
+      List<Object> values = new ArrayList<Object>(collection);
+      List<Object> otherValues = new ArrayList<Object>(otherCollection);
+      it : for (Iterator<Object> it = values.iterator(); it.hasNext();) {
+        Object value = it.next();
+        for (Iterator<Object> otherIt = otherValues.iterator(); otherIt.hasNext();) {
+          Object otherValue = otherIt.next();
+          if (sameOrEquals(value, otherValue, pending)) {
+            if (pairs != null) {
+              pairs.put(value, otherValue);
+            }
+            // If a match is found, remove both values from their lists
+            it.remove();
+            otherIt.remove();
+            continue it;
+          }
+        }
+        // A match for the value wasn't found
+        return false;
+      }
+      assert values.isEmpty() && otherValues.isEmpty();
+    }
+    return true;
+  }
+
+  /**
+   * Compare two Maps by size, and key-value pairs.
+   */
+  private static boolean sameOrEquals(Map<?, ?> map, Map<?, ?> otherMap,
+      Map<PendingComparison, Comparison> pending) {
+    if (map.size() != otherMap.size()) {
+      return false;
+    }
+    Map<Object, Object> pairs = new IdentityHashMap<Object, Object>();
+    if (!sameOrEquals(map.keySet(), otherMap.keySet(), pending, pairs)) {
+      return false;
+    }
+    for (Map.Entry<?, ?> entry : map.entrySet()) {
+      Object otherValue = otherMap.get(pairs.get(entry.getKey()));
+      if (!sameOrEquals(entry.getValue(), otherValue, pending)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Compare Splittables by kind and values.
+   */
+  private static boolean sameOrEquals(Splittable value, Splittable otherValue,
+      Map<PendingComparison, Comparison> pending) {
+    if (value == otherValue) {
+      return true;
+    }
+
+    // Strings
+    if (value.isString()) {
+      if (!otherValue.isString()) {
+        return false;
+      }
+      return value.asString().equals(otherValue.asString());
+    }
+
+    // Arrays
+    if (value.isIndexed()) {
+      if (!otherValue.isIndexed()) {
+        return false;
+      }
+
+      if (value.size() != otherValue.size()) {
+        return false;
+      }
+
+      for (int i = 0, j = value.size(); i < j; i++) {
+        if (!sameOrEquals(value.get(i), otherValue.get(i), pending)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    // Objects
+    if (value.isKeyed()) {
+      if (!otherValue.isKeyed()) {
+        return false;
+      }
+      /*
+       * We want to treat a missing property key as a null value, so we can't
+       * just compare the key lists.
+       */
+      List<String> keys = value.getPropertyKeys();
+      for (String key : keys) {
+        if (value.isNull(key)) {
+          // If value['foo'] is null, other['foo'] must also be null
+          if (!otherValue.isNull(key)) {
+            return false;
+          }
+        } else if (otherValue.isNull(key)
+            || !sameOrEquals(value.get(key), otherValue.get(key), pending)) {
+          return false;
+        }
+      }
+
+      // Look at keys only in otherValue, and ensure nullness
+      List<String> otherKeys = new ArrayList<String>(
+          otherValue.getPropertyKeys());
+      otherKeys.removeAll(keys);
+      for (String key : otherKeys) {
+        if (!value.isNull(key)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    // Unexpected
+    throw new UnsupportedOperationException("Splittable of unknown type");
+  }
+
+  /**
    * Utility class.
    */
   private AutoBeanUtils() {
diff --git a/user/src/com/google/gwt/autobean/shared/Splittable.java b/user/src/com/google/gwt/autobean/shared/Splittable.java
index 0059d3c..65a2286 100644
--- a/user/src/com/google/gwt/autobean/shared/Splittable.java
+++ b/user/src/com/google/gwt/autobean/shared/Splittable.java
@@ -50,6 +50,18 @@
   List<String> getPropertyKeys();
 
   /**
+   * Returns {@code} true if {@link #size()} and {@link #get(int)} can be
+   * expected to return meaningful values.
+   */
+  boolean isIndexed();
+
+  /**
+   * Returns {@code} true if {@link #getPropertyKeys()} and {@link #get(String)}
+   * can be expected to return meaningful values.
+   */
+  boolean isKeyed();
+
+  /**
    * Indicates if the nth element of a list is null.
    */
   boolean isNull(int index);
@@ -60,6 +72,12 @@
   boolean isNull(String key);
 
   /**
+   * Returns {@code} true if {@link #asString()} can be expected to return a
+   * meaningful value.
+   */
+  boolean isString();
+
+  /**
    * Returns the size of the list.
    */
   int size();
diff --git a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
index 3e804d1..2fb4749 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
@@ -57,6 +57,14 @@
     return split.getPropertyKeys();
   }
 
+  public boolean isIndexed() {
+    return payload.charAt(0) == '[';
+  }
+
+  public boolean isKeyed() {
+    return payload.charAt(0) == '{';
+  }
+
   public boolean isNull(int index) {
     maybeSplit();
     return split.isNull(index);
@@ -67,6 +75,10 @@
     return split.isNull(key);
   }
 
+  public boolean isString() {
+    return payload.charAt(0) == '\"';
+  }
+
   public int size() {
     maybeSplit();
     return split.size();
diff --git a/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java b/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java
new file mode 100644
index 0000000..26d9eef
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/DefaultProxyStore.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.shared;
+
+import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBeanCodex;
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.requestfactory.shared.impl.MessageFactoryHolder;
+import com.google.gwt.requestfactory.shared.messages.OperationMessage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An in-memory ProxyStore store that can encode its state as a JSON object
+ * literal.
+ */
+public class DefaultProxyStore implements ProxyStore {
+  /**
+   * Provide a little bit of future-proofing to at least detect an encoded
+   * payload that can't be decoded.
+   */
+  private static final String EXPECTED_VERSION = "211";
+  private final AutoBean<OperationMessage> messageBean;
+  private final Map<String, Splittable> map;
+
+  /**
+   * Construct an empty DefaultProxyStore.
+   */
+  public DefaultProxyStore() {
+    messageBean = MessageFactoryHolder.FACTORY.operation();
+    map = new HashMap<String, Splittable>();
+
+    OperationMessage message = messageBean.as();
+    message.setPropertyMap(map);
+    message.setVersion(EXPECTED_VERSION);
+  }
+
+  /**
+   * Construct a DefaultProxyStore using the a value returned from
+   * {@link #encode()}.
+   * 
+   * @param payload a String previously returned from {@link #encode()}
+   * @throws IllegalArgumentException if the payload cannot be parsed
+   */
+  public DefaultProxyStore(String payload) throws IllegalArgumentException {
+    messageBean = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
+        OperationMessage.class, payload);
+
+    OperationMessage message = messageBean.as();
+    if (!EXPECTED_VERSION.equals(message.getVersion())) {
+      throw new IllegalArgumentException(
+          "Unexpected version string in payload " + message.getVersion());
+    }
+    map = message.getPropertyMap();
+  }
+
+  /**
+   * Return a JSON object literal with the contents of the store.
+   */
+  public String encode() {
+    return AutoBeanCodex.encode(messageBean).getPayload();
+  }
+
+  public Splittable get(String key) {
+    return map.get(key);
+  }
+
+  public int nextId() {
+    return map.size();
+  }
+
+  public void put(String key, Splittable value) {
+    map.put(key, value);
+  }
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java b/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java
new file mode 100644
index 0000000..3cfcafd
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/ProxySerializer.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.shared;
+
+/**
+ * Serializes graphs of EntityProxy objects. A ProxySerializer is associated
+ * with an instance of a {@link ProxyStore} when it is created via
+ * {@link RequestFactory#getSerializer(ProxyStore)}.
+ * <p>
+ * The {@link EntityProxy#stableId()} of non-persisted (i.e. newly
+ * {@link RequestContext#create(Class) created}) {@link EntityProxy} instances
+ * are not stable.
+ * <p>
+ * To create a self-contained message that encapsulates a proxy:
+ * 
+ * <pre>
+ * RequestFactory myFactory = ...;
+ * MyFooProxy someProxy = ...;
+ * 
+ * DefaultProxyStore store = new DefaultProxyStore();
+ * ProxySerializer ser = myFactory.getSerializer(store);
+ * // More than one proxy could be serialized
+ * String key = ser.serialize(someProxy);
+ * // Create the flattened representation
+ * String payload = store.encode();
+ * </pre>
+ * 
+ * To recreate the object:
+ * 
+ * <pre>
+ * ProxyStore store = new DefaultProxyStore(payload);
+ * ProxySerializer ser = myFactory.getSerializer(store);
+ * MyFooProxy someProxy = ser.deserialize(MyFooProxy.class, key);
+ * </pre>
+ * 
+ * If two objects refer to different EntityProxy instances that have the same
+ * stableId(), the last mutable proxy encountered will be preferred, otherwise
+ * the first immutable proxy will be used.
+ * 
+ * @see DefaultProxyStore
+ */
+public interface ProxySerializer {
+  /**
+   * Recreate a proxy instance that was previously passed to
+   * {@link #serialize(BaseProxy)}.
+   * 
+   * @param <T> the type of proxy object to create
+   * @param proxyType the type of proxy object to create
+   * @param key a value previously returned from {@link #serialize(BaseProxy)}
+   * @return a new, immutable instance of the proxy or {@code null} if the data
+   *         needed to deserialize the proxy is not present in the ProxyStore
+   */
+  <T extends BaseProxy> T deserialize(Class<T> proxyType, String key);
+
+  /**
+   * Recreate a {@link EntityProxy} instance that was previously passed to
+   * {@link #serialize(BaseProxy)}.
+   * 
+   * @param <T> the type of proxy object to create
+   * @param id the {@link EntityProxyId} of the desired entity
+   * @return a new, immutable instance of the proxy or {@code null} if the data
+   *         needed to deserialize the proxy is not present in the ProxyStore
+   */
+  <T extends EntityProxy> T deserialize(EntityProxyId<T> id);
+
+  /**
+   * Store a proxy into the backing store.
+   * 
+   * @param proxy the proxy to store
+   * @return a key value that can be passed to
+   *         {@link #deserialize(Class, String)}
+   */
+  String serialize(BaseProxy proxy);
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java b/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java
new file mode 100644
index 0000000..412b070
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/ProxyStore.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.shared;
+
+import com.google.gwt.autobean.shared.Splittable;
+
+/**
+ * A ProxyStore provides a {@link ProxySerializer} with access to a low-level
+ * persistence mechanism. The ProxyStore does not need to be able to interpret
+ * the data sent to it by the ProxySerializer; it is merely a wrapper around a
+ * persistence mechanism.
+ * 
+ * @see DefaultProxyStore
+ */
+public interface ProxyStore {
+  /**
+   * Called by {@link ProxySerializer} to retrieve a value previously provided
+   * to {@link #put(String, Splittable)}.
+   * 
+   * @param key the key
+   * @return the associated value or {@code null} if {@code key} is unknown
+   */
+  Splittable get(String key);
+
+  /**
+   * Returns a non-negative sequence number. The actual sequence of values
+   * returned by this method is unimportant, as long as the numbers in the
+   * sequence are unique.
+   */
+  int nextId();
+
+  /**
+   * Called by {@link ProxySerializer} to store a value.
+   * 
+   * @param key a key value that will be passed to {@link #get(String)}
+   * @param value the data to store
+   * @see Splittable#getPayload()
+   */
+  void put(String key, Splittable value);
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
index 8e5778c..16d116c 100644
--- a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
@@ -114,6 +114,17 @@
   <T extends EntityProxy> EntityProxyId<T> getProxyId(String historyToken);
 
   /**
+   * Returns a ProxySerializer that can encode and decode the various
+   * EntityProxy and ValueProxy types reachable from the RequestFactory.
+   * 
+   * @param store a helper object for the ProxySerializer to provide low-level
+   *          storage access
+   * @return a new ProxySerializer
+   * @see DefaultProxyStore
+   */
+  ProxySerializer getSerializer(ProxyStore store);
+
+  /**
    * Start this request factory with a
    * {@link com.google.gwt.requestfactory.client.DefaultRequestTransport}.
    * 
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
index 43c766e..2a45831 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -123,7 +123,7 @@
    * Objects are placed into this map by being passed into {@link #edit} or as
    * an invocation argument.
    */
-  private final Map<SimpleProxyId<?>, AutoBean<?>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<?>>();
+  private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>();
   /**
    * A map that contains the canonical instance of an entity to return in the
    * return graph, since this is built from scratch.
@@ -300,6 +300,176 @@
   }
 
   /**
+   * Resolves an IdMessage into an SimpleProxyId.
+   */
+  SimpleProxyId<BaseProxy> getId(IdMessage op) {
+    if (Strength.SYNTHETIC.equals(op.getStrength())) {
+      return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
+    }
+    return requestFactory.getId(op.getTypeToken(), op.getServerId(),
+        op.getClientId());
+  }
+
+  /**
+   * Creates or retrieves a new canonical AutoBean to represent the given id in
+   * the returned payload.
+   */
+  <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(
+      SimpleProxyId<Q> id) {
+    @SuppressWarnings("unchecked")
+    AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id);
+    if (bean == null) {
+      Class<Q> proxyClass = id.getProxyClass();
+      bean = requestFactory.createProxy(proxyClass, id);
+      returnedProxies.put(id, bean);
+    }
+
+    return bean;
+  }
+
+  /**
+   * Create a single OperationMessage that encapsulates the state of a proxy
+   * AutoBean.
+   */
+  AutoBean<OperationMessage> makeOperationMessage(
+      SimpleProxyId<BaseProxy> stableId, AutoBean<?> proxyBean, boolean useDelta) {
+
+    // The OperationMessages describes operations on exactly one entity
+    AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation();
+    OperationMessage operation = toReturn.as();
+    operation.setTypeToken(requestFactory.getTypeToken(stableId.getProxyClass()));
+
+    // Find the object to compare against
+    AutoBean<?> parent;
+    if (stableId.isEphemeral()) {
+      // Newly-created object, use a blank object to compare against
+      parent = requestFactory.createProxy(stableId.getProxyClass(), stableId);
+
+      // Newly-created objects go into the persist operation bucket
+      operation.setOperation(WriteOperation.PERSIST);
+      // The ephemeral id is passed to the server
+      operation.setClientId(stableId.getClientId());
+      operation.setStrength(Strength.EPHEMERAL);
+    } else if (stableId.isSynthetic()) {
+      // Newly-created object, use a blank object to compare against
+      parent = requestFactory.createProxy(stableId.getProxyClass(), stableId);
+
+      // Newly-created objects go into the persist operation bucket
+      operation.setOperation(WriteOperation.PERSIST);
+      // The ephemeral id is passed to the server
+      operation.setSyntheticId(stableId.getSyntheticId());
+      operation.setStrength(Strength.SYNTHETIC);
+    } else {
+      parent = proxyBean.getTag(PARENT_OBJECT);
+      // Requests involving existing objects use the persisted id
+      operation.setServerId(stableId.getServerId());
+      operation.setOperation(WriteOperation.UPDATE);
+    }
+    assert !useDelta || parent != null;
+
+    // Send our version number to the server to cut down on future payloads
+    String version = proxyBean.getTag(Constants.VERSION_PROPERTY_B64);
+    if (version != null) {
+      operation.setVersion(version);
+    }
+
+    Map<String, Object> diff = Collections.emptyMap();
+    if (isEntityType(stableId.getProxyClass())) {
+      // Compute what's changed on the client
+      diff = useDelta ? AutoBeanUtils.diff(parent, proxyBean)
+          : AutoBeanUtils.getAllProperties(proxyBean);
+    } else if (isValueType(stableId.getProxyClass())) {
+      // Send everything
+      diff = AutoBeanUtils.getAllProperties(proxyBean);
+    }
+
+    if (!diff.isEmpty()) {
+      Map<String, Splittable> propertyMap = new HashMap<String, Splittable>();
+      for (Map.Entry<String, Object> entry : diff.entrySet()) {
+        propertyMap.put(entry.getKey(),
+            EntityCodex.encode(this, entry.getValue()));
+      }
+      operation.setPropertyMap(propertyMap);
+    }
+    return toReturn;
+  }
+
+  /**
+   * Create a new EntityProxy from a snapshot in the return payload.
+   * 
+   * @param id the EntityProxyId of the object
+   * @param returnRecord the JSON map containing property/value pairs
+   * @param operations the WriteOperation eventns to broadcast over the EventBus
+   */
+  <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id,
+      OperationMessage op, WriteOperation... operations) {
+
+    AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id);
+    toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion());
+
+    final Map<String, Splittable> properties = op.getPropertyMap();
+    if (properties != null) {
+      // Apply updates
+      toMutate.accept(new AutoBeanVisitor() {
+        @Override
+        public boolean visitReferenceProperty(String propertyName,
+            AutoBean<?> value, PropertyContext ctx) {
+          if (ctx.canSet()) {
+            if (properties.containsKey(propertyName)) {
+              Splittable raw = properties.get(propertyName);
+              Class<?> elementType = ctx instanceof CollectionPropertyContext
+                  ? ((CollectionPropertyContext) ctx).getElementType() : null;
+              Object decoded = EntityCodex.decode(AbstractRequestContext.this,
+                  ctx.getType(), elementType, raw);
+              ctx.set(decoded);
+            }
+          }
+          return false;
+        }
+
+        @Override
+        public boolean visitValueProperty(String propertyName, Object value,
+            PropertyContext ctx) {
+          if (ctx.canSet()) {
+            if (properties.containsKey(propertyName)) {
+              Splittable raw = properties.get(propertyName);
+              Object decoded = ValueCodex.decode(ctx.getType(), raw);
+              // Hack for Date, consider generalizing for "custom serializers"
+              if (decoded != null && Date.class.equals(ctx.getType())) {
+                decoded = new DatePoser((Date) decoded);
+              }
+              ctx.set(decoded);
+            }
+          }
+          return false;
+        }
+      });
+    }
+
+    // Finished applying updates, freeze the bean
+    makeImmutable(toMutate);
+    Q proxy = toMutate.as();
+
+    /*
+     * Notify subscribers if the object differs from when it first came into the
+     * RequestContext.
+     */
+    if (operations != null && requestFactory.isEntityType(id.getProxyClass())) {
+      for (WriteOperation writeOperation : operations) {
+        if (writeOperation.equals(WriteOperation.UPDATE)
+            && !requestFactory.hasVersionChanged(id, op.getVersion())) {
+          // No updates if the server reports no change
+          continue;
+        }
+        requestFactory.getEventBus().fireEventFromSource(
+            new EntityProxyChange<EntityProxy>((EntityProxy) proxy,
+                writeOperation), id.getProxyClass());
+      }
+    }
+    return proxy;
+  }
+
+  /**
    * Get-or-create method for synthetic ids.
    * 
    * @see #syntheticIds
@@ -515,34 +685,6 @@
   }
 
   /**
-   * Resolves an IdMessage into an SimpleProxyId.
-   */
-  private SimpleProxyId<BaseProxy> getId(IdMessage op) {
-    if (Strength.SYNTHETIC.equals(op.getStrength())) {
-      return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
-    }
-    return requestFactory.getId(op.getTypeToken(), op.getServerId(),
-        op.getClientId());
-  }
-
-  /**
-   * Creates or retrieves a new canonical AutoBean to represent the given id in
-   * the returned payload.
-   */
-  private <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(
-      SimpleProxyId<Q> id) {
-    @SuppressWarnings("unchecked")
-    AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id);
-    if (bean == null) {
-      Class<Q> proxyClass = id.getProxyClass();
-      bean = requestFactory.createProxy(proxyClass, id);
-      returnedProxies.put(id, bean);
-    }
-
-    return bean;
-  }
-
-  /**
    * Make an EntityProxy immutable.
    */
   private void makeImmutable(final AutoBean<? extends BaseProxy> toMutate) {
@@ -566,88 +708,8 @@
     // Get the factory from the runtime-specific holder.
     MessageFactory f = MessageFactoryHolder.FACTORY;
 
-    List<OperationMessage> operations = new ArrayList<OperationMessage>();
-    // Compute deltas for each entity seen by the context
-    for (AutoBean<?> currentView : editedProxies.values()) {
-      @SuppressWarnings("unchecked")
-      SimpleProxyId<BaseProxy> stableId = stableId((AutoBean<BaseProxy>) currentView);
-      assert !stableId.isSynthetic() : "Should not send synthetic id to server";
-
-      // The OperationMessages describes operations on exactly one entity
-      OperationMessage operation = f.operation().as();
-      operation.setTypeToken(requestFactory.getTypeToken(stableId.getProxyClass()));
-
-      // Find the object to compare against
-      AutoBean<?> parent;
-      if (stableId.isEphemeral()) {
-        // Newly-created object, use a blank object to compare against
-        parent = requestFactory.createProxy(stableId.getProxyClass(), stableId);
-
-        // Newly-created objects go into the persist operation bucket
-        operation.setOperation(WriteOperation.PERSIST);
-        // The ephemeral id is passed to the server
-        operation.setClientId(stableId.getClientId());
-        operation.setStrength(Strength.EPHEMERAL);
-      } else {
-        parent = currentView.getTag(PARENT_OBJECT);
-        // Requests involving existing objects use the persisted id
-        operation.setServerId(stableId.getServerId());
-        operation.setOperation(WriteOperation.UPDATE);
-      }
-      assert parent != null;
-
-      // Send our version number to the server to cut down on future payloads
-      String version = currentView.getTag(Constants.VERSION_PROPERTY_B64);
-      if (version != null) {
-        operation.setVersion(version);
-      }
-
-      Map<String, Object> diff = Collections.emptyMap();
-      if (isEntityType(stableId.getProxyClass())) {
-        // Compute what's changed on the client
-        diff = AutoBeanUtils.diff(parent, currentView);
-      } else if (isValueType(stableId.getProxyClass())) {
-        // Send everything
-        diff = AutoBeanUtils.getAllProperties(currentView);
-      }
-
-      if (!diff.isEmpty()) {
-        Map<String, Splittable> propertyMap = new HashMap<String, Splittable>();
-        for (Map.Entry<String, Object> entry : diff.entrySet()) {
-          propertyMap.put(entry.getKey(),
-              EntityCodex.encode(this, entry.getValue()));
-        }
-        operation.setPropertyMap(propertyMap);
-      }
-      operations.add(operation);
-    }
-
-    List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>();
-    for (AbstractRequest<?> invocation : invocations) {
-      RequestData data = invocation.getRequestData();
-      InvocationMessage message = f.invocation().as();
-
-      String opsToSend = data.getOperation();
-      if (!opsToSend.isEmpty()) {
-        message.setOperation(opsToSend);
-      }
-
-      Set<String> refsToSend = data.getPropertyRefs();
-      if (!refsToSend.isEmpty()) {
-        message.setPropertyRefs(refsToSend);
-      }
-
-      List<Splittable> parameters = new ArrayList<Splittable>(
-          data.getParameters().length);
-      for (Object param : data.getParameters()) {
-        parameters.add(EntityCodex.encode(this, param));
-      }
-      if (!parameters.isEmpty()) {
-        message.setParameters(parameters);
-      }
-
-      invocationMessages.add(message);
-    }
+    List<OperationMessage> operations = makePayloadOperations();
+    List<InvocationMessage> invocationMessages = makePayloadInvocations();
 
     // Create the outer envelope message
     AutoBean<RequestMessage> bean = f.request();
@@ -662,78 +724,53 @@
   }
 
   /**
-   * Create a new EntityProxy from a snapshot in the return payload.
-   * 
-   * @param id the EntityProxyId of the object
-   * @param returnRecord the JSON map containing property/value pairs
-   * @param operations the WriteOperation eventns to broadcast over the EventBus
+   * Create an InvocationMessage for each remote method call being made by the
+   * context.
    */
-  private <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id,
-      OperationMessage op, WriteOperation... operations) {
+  private List<InvocationMessage> makePayloadInvocations() {
+    MessageFactory f = MessageFactoryHolder.FACTORY;
 
-    AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id);
-    toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion());
+    List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>();
+    for (AbstractRequest<?> invocation : invocations) {
+      // RequestData is produced by the generated subclass
+      RequestData data = invocation.getRequestData();
+      InvocationMessage message = f.invocation().as();
 
-    final Map<String, Splittable> properties = op.getPropertyMap();
-    if (properties != null) {
-      // Apply updates
-      toMutate.accept(new AutoBeanVisitor() {
-        @Override
-        public boolean visitReferenceProperty(String propertyName,
-            AutoBean<?> value, PropertyContext ctx) {
-          if (ctx.canSet()) {
-            if (properties.containsKey(propertyName)) {
-              Splittable raw = properties.get(propertyName);
-              Class<?> elementType = ctx instanceof CollectionPropertyContext
-                  ? ((CollectionPropertyContext) ctx).getElementType() : null;
-              Object decoded = EntityCodex.decode(AbstractRequestContext.this,
-                  ctx.getType(), elementType, raw);
-              ctx.set(decoded);
-            }
-          }
-          return false;
-        }
+      // Operation; essentially a method descriptor
+      message.setOperation(data.getOperation());
 
-        @Override
-        public boolean visitValueProperty(String propertyName, Object value,
-            PropertyContext ctx) {
-          if (ctx.canSet()) {
-            if (properties.containsKey(propertyName)) {
-              Splittable raw = properties.get(propertyName);
-              Object decoded = ValueCodex.decode(ctx.getType(), raw);
-              // Hack for Date, consider generalizing for "custom serializers"
-              if (Date.class.equals(ctx.getType())) {
-                decoded = new DatePoser((Date) decoded);
-              }
-              ctx.set(decoded);
-            }
-          }
-          return false;
-        }
-      });
-    }
-
-    // Finished applying updates, freeze the bean
-    makeImmutable(toMutate);
-    Q proxy = toMutate.as();
-
-    /*
-     * Notify subscribers if the object differs from when it first came into the
-     * RequestContext.
-     */
-    if (operations != null && requestFactory.isEntityType(id.getProxyClass())) {
-      for (WriteOperation writeOperation : operations) {
-        if (writeOperation.equals(WriteOperation.UPDATE)
-            && !requestFactory.hasVersionChanged(id, op.getVersion())) {
-          // No updates if the server reports no change
-          continue;
-        }
-        requestFactory.getEventBus().fireEventFromSource(
-            new EntityProxyChange<EntityProxy>((EntityProxy) proxy,
-                writeOperation), id.getProxyClass());
+      // The arguments to the with() calls
+      Set<String> refsToSend = data.getPropertyRefs();
+      if (!refsToSend.isEmpty()) {
+        message.setPropertyRefs(refsToSend);
       }
+
+      // Parameter values or references
+      List<Splittable> parameters = new ArrayList<Splittable>(
+          data.getParameters().length);
+      for (Object param : data.getParameters()) {
+        parameters.add(EntityCodex.encode(this, param));
+      }
+      if (!parameters.isEmpty()) {
+        message.setParameters(parameters);
+      }
+
+      invocationMessages.add(message);
     }
-    return proxy;
+    return invocationMessages;
+  }
+
+  /**
+   * Compute deltas for each entity seen by the context.
+   */
+  private List<OperationMessage> makePayloadOperations() {
+    List<OperationMessage> operations = new ArrayList<OperationMessage>();
+    for (AutoBean<? extends BaseProxy> currentView : editedProxies.values()) {
+      OperationMessage operation = makeOperationMessage(
+          BaseProxyCategory.stableId(currentView), currentView, true).as();
+      operations.add(operation);
+    }
+    return operations;
   }
 
   /**
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
index b9c95ce..b5ffeda 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -23,6 +23,8 @@
 import com.google.gwt.requestfactory.shared.BaseProxy;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.ProxySerializer;
+import com.google.gwt.requestfactory.shared.ProxyStore;
 import com.google.gwt.requestfactory.shared.Request;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestTransport;
@@ -114,6 +116,10 @@
     return transport;
   }
 
+  public ProxySerializer getSerializer(ProxyStore store) {
+    return new ProxySerializerImpl(this, store);
+  }
+
   /**
    * The choice of a default request transport is runtime-specific.
    */
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
index 0c16cae..aa3254d 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
@@ -49,6 +49,7 @@
    */
   public <P extends BaseProxy> SimpleProxyId<P> allocateSyntheticId(
       Class<P> clazz, int syntheticId) {
+    assert syntheticId > 0;
     SimpleProxyId<P> toReturn = createId(clazz, "%" + syntheticId);
     toReturn.setSyntheticId(syntheticId);
     return toReturn;
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
new file mode 100644
index 0000000..73b9c55
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.requestfactory.shared.impl;
+
+import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBeanCodex;
+import com.google.gwt.autobean.shared.AutoBeanUtils;
+import com.google.gwt.autobean.shared.AutoBeanVisitor;
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.requestfactory.shared.BaseProxy;
+import com.google.gwt.requestfactory.shared.EntityProxy;
+import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.ProxySerializer;
+import com.google.gwt.requestfactory.shared.ProxyStore;
+import com.google.gwt.requestfactory.shared.messages.IdMessage;
+import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
+import com.google.gwt.requestfactory.shared.messages.OperationMessage;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The default implementation of ProxySerializer.
+ */
+class ProxySerializerImpl extends AbstractRequestContext implements
+    ProxySerializer {
+
+  /**
+   * Used internally to unwind the stack if data cannot be found in the backing
+   * store.
+   */
+  private static class NoDataException extends RuntimeException {
+  }
+
+  private final ProxyStore store;
+  /**
+   * If the user wants to serialize a proxy with a non-persistent id (including
+   * ValueProxy), we'll assign a synthetic id that is local to the store being
+   * used.
+   */
+  private final Map<SimpleProxyId<?>, SimpleProxyId<?>> syntheticIds = new HashMap<SimpleProxyId<?>, SimpleProxyId<?>>();
+
+  /**
+   * The ids of proxies whose content has been reloaded.
+   */
+  private final Set<SimpleProxyId<?>> restored = new HashSet<SimpleProxyId<?>>();
+  private final Map<SimpleProxyId<?>, AutoBean<?>> serialized = new HashMap<SimpleProxyId<?>, AutoBean<?>>();
+
+  public ProxySerializerImpl(AbstractRequestFactory factory, ProxyStore store) {
+    super(factory);
+    this.store = store;
+  }
+
+  public <T extends BaseProxy> T deserialize(Class<T> proxyType, String key) {
+    // Fast exit to prevent getOperation from throwing an exception
+    if (store.get(key) == null) {
+      return null;
+    }
+    OperationMessage op = getOperation(proxyType, key);
+    @SuppressWarnings("unchecked")
+    SimpleProxyId<T> id = (SimpleProxyId<T>) getId(op);
+    return doDeserialize(id);
+  }
+
+  public <T extends EntityProxy> T deserialize(EntityProxyId<T> id) {
+    return doDeserialize((SimpleEntityProxyId<T>) id);
+  }
+
+  /**
+   * Replace non-persistent ids with store-local ids.
+   */
+  @Override
+  public Splittable getSerializedProxyId(SimpleProxyId<?> stableId) {
+    return super.getSerializedProxyId(serializedId(stableId));
+  }
+
+  public String serialize(BaseProxy rootObject) {
+    final AutoBean<? extends BaseProxy> root = AutoBeanUtils.getAutoBean(rootObject);
+    if (root == null) {
+      // Unexpected, some kind of foreign implementation of the BaseProxy?
+      throw new IllegalArgumentException();
+    }
+
+    final SimpleProxyId<?> id = serializedId(BaseProxyCategory.stableId(root));
+    // Only persistent and synthetic ids expected
+    assert !id.isEphemeral() : "Unexpected ephemeral id " + id.toString();
+
+    /*
+     * Don't repeatedly serialize the same proxy, unless we're looking at a
+     * mutable instance.
+     */
+    AutoBean<?> previous = serialized.get(id);
+    if (previous == null || !previous.isFrozen()) {
+      serialized.put(id, root);
+      serializeOneProxy(id, root);
+      root.accept(new AutoBeanVisitor() {
+        @Override
+        public void endVisit(AutoBean<?> bean, Context ctx) {
+          // Avoid unnecessary method call
+          if (bean == root) {
+            return;
+          }
+          if (isEntityType(bean.getType()) || isValueType(bean.getType())) {
+            serialize((BaseProxy) bean.as());
+          }
+        }
+
+        @Override
+        public void endVisitCollectionProperty(String propertyName,
+            AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
+          if (value == null) {
+            return;
+          }
+          if (isEntityType(ctx.getElementType())
+              || isValueType(ctx.getElementType())) {
+            for (Object o : value.as()) {
+              serialize((BaseProxy) o);
+            }
+          }
+        }
+      });
+    }
+
+    return getRequestFactory().getHistoryToken(id);
+  }
+
+  @Override
+  SimpleProxyId<BaseProxy> getId(IdMessage op) {
+    if (Strength.SYNTHETIC.equals(op.getStrength())) {
+      return getRequestFactory().allocateSyntheticId(
+          getRequestFactory().getTypeFromToken(op.getTypeToken()),
+          op.getSyntheticId());
+    }
+    return super.getId(op);
+  }
+
+  @Override
+  <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(
+      SimpleProxyId<Q> id) {
+    AutoBean<Q> toReturn = super.getProxyForReturnPayloadGraph(id);
+    if (restored.add(id)) {
+      /*
+       * If we haven't seen the id before, use the data in the OperationMessage
+       * to repopulate the properties of the canonical bean for this id.
+       */
+      OperationMessage op = getOperation(id.getProxyClass(),
+          getRequestFactory().getHistoryToken(id));
+      this.processReturnOperation(id, op);
+      toReturn.setTag(Constants.STABLE_ID, super.getId(op));
+    }
+    return toReturn;
+  }
+
+  /**
+   * Reset all temporary state.
+   */
+  private void clear() {
+    syntheticIds.clear();
+    restored.clear();
+    serialized.clear();
+  }
+
+  private <T extends BaseProxy> T doDeserialize(SimpleProxyId<T> id) {
+    try {
+      return getProxyForReturnPayloadGraph(id).as();
+    } catch (NoDataException e) {
+      return null;
+    } finally {
+      clear();
+    }
+  }
+
+  /**
+   * Load the OperationMessage containing the object state from the backing
+   * store.
+   */
+  private <T> OperationMessage getOperation(Class<T> proxyType, String key) {
+    Splittable data = store.get(key);
+    if (data == null) {
+      throw new NoDataException();
+    }
+
+    OperationMessage op = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
+        OperationMessage.class, data).as();
+    return op;
+  }
+
+  /**
+   * Convert any non-persistent ids into store-local synthetic ids.
+   */
+  private <T extends BaseProxy> SimpleProxyId<T> serializedId(
+      SimpleProxyId<T> stableId) {
+    assert !stableId.isSynthetic();
+    if (stableId.isEphemeral()) {
+      @SuppressWarnings("unchecked")
+      SimpleProxyId<T> syntheticId = (SimpleProxyId<T>) syntheticIds.get(stableId);
+      if (syntheticId == null) {
+        int nextId = store.nextId();
+        assert nextId >= 0 : "ProxyStore.nextId() returned a negative number "
+            + nextId;
+        syntheticId = getRequestFactory().allocateSyntheticId(
+            stableId.getProxyClass(), nextId + 1);
+        syntheticIds.put(stableId, syntheticId);
+      }
+      return syntheticId;
+    }
+    return stableId;
+  }
+
+  private void serializeOneProxy(SimpleProxyId<?> idForSerialization,
+      AutoBean<? extends BaseProxy> bean) {
+    AutoBean<OperationMessage> op = makeOperationMessage(
+        serializedId(BaseProxyCategory.stableId(bean)), bean, false);
+
+    store.put(getRequestFactory().getHistoryToken(idForSerialization),
+        AutoBeanCodex.encode(op));
+  }
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java
index b2b3422..5550921 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/ValueProxyCategory.java
@@ -22,7 +22,7 @@
 import com.google.gwt.requestfactory.shared.ValueProxy;
 
 /**
- * Contains static implementation of EntityProxy-specific methods.
+ * Contains static implementation of ValueProxy-specific methods.
  */
 public class ValueProxyCategory {
 
@@ -44,12 +44,8 @@
       return false;
     }
 
-    /*
-     * Comparison of ValueProxies is based solely on property values. Unlike an
-     * EntityProxy, neither the id nor the RequestContext is used
-     */
-    return AutoBeanUtils.getAllProperties(bean).equals(
-        AutoBeanUtils.getAllProperties(other));
+    // Compare the entire object graph
+    return AutoBeanUtils.deepEquals(bean, other);
   }
 
   /**
diff --git a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java
index 7346bec..3f1c489 100644
--- a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java
+++ b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -29,6 +29,14 @@
   }
 
   public static Splittable split(String payload) {
-    return JsoSplittable.create(JsonUtils.safeEval(payload));
+    boolean isString = payload.charAt(0) == '\"';
+    if (isString) {
+      payload = "[" + payload + "]";
+    }
+    Splittable toReturn = JsoSplittable.create(JsonUtils.safeEval(payload));
+    if (isString) {
+      toReturn = toReturn.get(0);
+    }
+    return toReturn;
   }
 }
diff --git a/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java b/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
index 3e5d3ec..b3729d6 100644
--- a/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
+++ b/user/super/com/google/gwt/requestfactory/super/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
@@ -21,6 +21,6 @@
 /**
  * This a super-source version with a client-only implementation.
  */
-interface MessageFactoryHolder {
+public interface MessageFactoryHolder {
   MessageFactory FACTORY = GWT.create(MessageFactory.class);
 }
diff --git a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
index 54c7767..c9484a1 100644
--- a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
+++ b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
@@ -73,11 +73,11 @@
     boolean getGet();
 
     boolean hasHas();
-    
+
     void setIs(boolean value);
 
     void setGet(boolean value);
-    
+
     void setHas(boolean value);
   }
 
@@ -189,6 +189,7 @@
     assertNotSame(i1, i2);
     assertFalse(i1.equals(i2));
     assertEquals(i1.getInt(), i2.getInt());
+    assertTrue(AutoBeanUtils.deepEquals(a1, a2));
 
     // Cloned instances do not affect one another
     i1.setInt(41);
@@ -204,6 +205,8 @@
     o1.getIntf().setInt(42);
 
     AutoBean<OtherIntf> a2 = a1.clone(true);
+    assertTrue(AutoBeanUtils.deepEquals(a1, a2));
+
     OtherIntf o2 = a2.as();
 
     assertNotSame(o1.getIntf(), o2.getIntf());
diff --git a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
index 5b4d0d6..027d454 100644
--- a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
+++ b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
@@ -101,9 +101,6 @@
 
     Map<String, Simple> getSimpleMap();
 
-    @AutoBean.PropertyName("simpleMap")
-    Map<String, ReachableOnlyFromParameterization> getSimpleMapAltType();
-
     void setComplexMap(Map<Simple, Simple> map);
 
     void setSimpleMap(Map<String, Simple> map);
@@ -146,7 +143,7 @@
     AutoBean<HasCycle> bean = f.hasCycle();
     bean.as().setCycle(Arrays.asList(bean.as()));
     try {
-      AutoBeanCodex.encode(bean);
+      checkEncode(bean);
       fail("Should not have encoded");
     } catch (UnsupportedOperationException expected) {
     }
@@ -155,13 +152,18 @@
   public void testEmptyList() {
     AutoBean<HasList> bean = f.hasList();
     bean.as().setList(Collections.<Simple> emptyList());
-    Splittable split = AutoBeanCodex.encode(bean);
-    AutoBean<HasList> decodedBean = AutoBeanCodex.decode(f, HasList.class,
-        split);
+    AutoBean<HasList> decodedBean = checkEncode(bean);
     assertNotNull(decodedBean.as().getList());
     assertTrue(decodedBean.as().getList().isEmpty());
   }
 
+  private <T> AutoBean<T> checkEncode(AutoBean<T> bean) {
+    Splittable split = AutoBeanCodex.encode(bean);
+    AutoBean<T> decoded = AutoBeanCodex.decode(f, bean.getType(), split);
+    assertTrue(AutoBeanUtils.deepEquals(bean, decoded));
+    return decoded;
+  }
+
   public void testEnum() {
     EnumMap map = (EnumMap) f;
     assertEquals("BAR", map.getToken(MyEnum.BAR));
@@ -184,7 +186,7 @@
     // Make sure the overridden form is always used
     assertFalse(split.getPayload().contains("BAZ"));
 
-    AutoBean<HasEnum> decoded = AutoBeanCodex.decode(f, HasEnum.class, split);
+    AutoBean<HasEnum> decoded = checkEncode(bean);
     assertEquals(MyEnum.BAZ, decoded.as().getEnum());
     assertEquals(arrayValue, decoded.as().getEnums());
     assertEquals(mapValue, decoded.as().getMap());
@@ -207,8 +209,7 @@
       complex.put(key, s);
     }
 
-    Splittable split = AutoBeanCodex.encode(bean);
-    AutoBean<HasMap> decoded = AutoBeanCodex.decode(f, HasMap.class, split);
+    AutoBean<HasMap> decoded = checkEncode(bean);
     map = decoded.as().getSimpleMap();
     complex = decoded.as().getComplexMap();
     assertEquals(5, map.size());
@@ -226,8 +227,7 @@
 
   public void testNull() {
     AutoBean<Simple> bean = f.simple();
-    Splittable split = AutoBeanCodex.encode(bean);
-    AutoBean<Simple> decodedBean = AutoBeanCodex.decode(f, Simple.class, split);
+    AutoBean<Simple> decodedBean = checkEncode(bean);
     assertNull(decodedBean.as().getString());
   }
 
@@ -237,17 +237,13 @@
     simple.setInt(42);
     simple.setString("Hello World!");
 
-    Splittable split = AutoBeanCodex.encode(bean);
-
-    AutoBean<Simple> decodedBean = AutoBeanCodex.decode(f, Simple.class, split);
+    AutoBean<Simple> decodedBean = checkEncode(bean);
     assertTrue(AutoBeanUtils.diff(bean, decodedBean).isEmpty());
 
     AutoBean<HasSimple> bean2 = f.hasSimple();
     bean2.as().setSimple(simple);
-    split = AutoBeanCodex.encode(bean2);
 
-    AutoBean<HasSimple> decodedBean2 = AutoBeanCodex.decode(f, HasSimple.class,
-        split);
+    AutoBean<HasSimple> decodedBean2 = checkEncode(bean2);
     assertNotNull(decodedBean2.as().getSimple());
     assertTrue(AutoBeanUtils.diff(bean,
         AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple())).isEmpty());
@@ -255,10 +251,8 @@
     AutoBean<HasList> bean3 = f.hasList();
     bean3.as().setIntList(Arrays.asList(1, 2, 3, null, 4, 5));
     bean3.as().setList(Arrays.asList(simple));
-    split = AutoBeanCodex.encode(bean3);
 
-    AutoBean<HasList> decodedBean3 = AutoBeanCodex.decode(f, HasList.class,
-        split);
+    AutoBean<HasList> decodedBean3 = checkEncode(bean3);
     assertNotNull(decodedBean3.as().getIntList());
     assertEquals(Arrays.asList(1, 2, 3, null, 4, 5),
         decodedBean3.as().getIntList());
@@ -277,10 +271,8 @@
     List<Splittable> testList = Arrays.asList(AutoBeanCodex.encode(simple),
         null, AutoBeanCodex.encode(simple));
     bean.as().setSimpleList(testList);
-    Splittable split = AutoBeanCodex.encode(bean);
 
-    AutoBean<HasAutoBean> decoded = AutoBeanCodex.decode(f, HasAutoBean.class,
-        split);
+    AutoBean<HasAutoBean> decoded = checkEncode(bean);
     Splittable toDecode = decoded.as().getSimple();
     AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class,
         toDecode);
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
index a52ab68..b512857 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -44,9 +44,11 @@
  * Tests for {@link com.google.gwt.requestfactory.shared.RequestFactory}.
  */
 public class RequestFactoryTest extends RequestFactoryTestBase {
-
   /*
    * DO NOT USE finishTest(). Instead, call finishTestAndReset();
+   * 
+   * When possible, pass any returned proxies to checkSerialization() and use
+   * the return value in the place of the returned object.
    */
 
   class FooReciever extends Receiver<SimpleFooProxy> {
@@ -80,6 +82,7 @@
       persistRequest.fire(new Receiver<SimpleFooProxy>() {
         @Override
         public void onSuccess(SimpleFooProxy response) {
+          response = checkSerialization(response);
           finishTestAndReset();
         }
       });
@@ -206,6 +209,7 @@
         new Receiver<SimpleBarProxy>() {
           @Override
           public void onSuccess(SimpleBarProxy persistentBar) {
+            persistentBar = checkSerialization(persistentBar);
             // Persist foo with bar as a child.
             SimpleFooRequest context = req.simpleFooRequest();
             SimpleFooProxy foo = context.create(SimpleFooProxy.class);
@@ -217,6 +221,7 @@
             persistRequest.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy persistentFoo) {
+                persistentFoo = checkSerialization(persistentFoo);
                 // Handle changes to SimpleFooProxy.
                 final SimpleFooEventHandler<SimpleFooProxy> fooHandler = new SimpleFooEventHandler<SimpleFooProxy>();
                 EntityProxyChange.registerForProxyType(req.getEventBus(),
@@ -265,6 +270,7 @@
         "selfOneToManyField").fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         assertNotNull(response.getFooField());
         assertSame(response.getFooField(),
             response.getSelfOneToManyField().get(0));
@@ -297,6 +303,7 @@
     request.fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         assertFalse(((SimpleEntityProxyId<SimpleFooProxy>) response.stableId()).isEphemeral());
         assertEquals(2, handler.persistEventCount); // two bars persisted.
         assertEquals(2, handler.updateEventCount); // two bars persisted.
@@ -329,6 +336,7 @@
 
           @Override
           public void onSuccess(SimpleFooProxy foo) {
+            foo = checkSerialization(foo);
             SimpleFooRequest context = simpleFooRequest();
 
             // edit() doesn't cause a change
@@ -376,6 +384,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             assertEquals(
                 "I'm here",
                 response.getSelfOneToManyField().get(0).getFooField().getUserName());
@@ -390,6 +399,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             assertEquals(42, response.getIntId().intValue());
             finishTestAndReset();
           }
@@ -411,7 +421,8 @@
     fooReq.fire(new Receiver<SimpleFooProxy>() {
 
       @Override
-      public void onSuccess(final SimpleFooProxy returned) {
+      public void onSuccess(SimpleFooProxy returned) {
+        returned = checkSerialization(returned);
         EntityProxyId<SimpleFooProxy> returnedId = returned.stableId();
         assertEquals(futureId, returnedId);
         assertFalse((((SimpleEntityProxyId<?>) returnedId).isEphemeral()));
@@ -435,7 +446,8 @@
     fooReq.fire(new Receiver<SimpleBarProxy>() {
 
       @Override
-      public void onSuccess(final SimpleBarProxy returned) {
+      public void onSuccess(SimpleBarProxy returned) {
+        returned = checkSerialization(returned);
         assertFalse(((SimpleEntityProxyId<?>) foo.stableId()).isEphemeral());
 
         checkStableIdEquals(foo, returned);
@@ -454,7 +466,8 @@
     fooReq.fire(new Receiver<SimpleBarProxy>() {
 
       @Override
-      public void onSuccess(final SimpleBarProxy returned) {
+      public void onSuccess(SimpleBarProxy returned) {
+        returned = checkSerialization(returned);
         assertFalse(((SimpleEntityProxyId<?>) bar.stableId()).isEphemeral());
         assertFalse(((SimpleEntityProxyId<?>) returned.stableId()).isEphemeral());
 
@@ -496,6 +509,7 @@
     contextA.findSimpleFooById(999L).fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         // The response shouldn't be associated with a RequestContext
         contextB.edit(response);
         finishTestAndReset();
@@ -509,6 +523,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             assertEquals(42, (int) response.getIntId());
             assertEquals("GWT", response.getUserName());
             assertEquals(8L, (long) response.getLongField());
@@ -526,6 +541,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             assertEquals(42, (int) response.getIntId());
             assertEquals("GWT", response.getUserName());
             assertEquals(8L, (long) response.getLongField());
@@ -542,7 +558,7 @@
     simpleFooRequest().findAll().fire(new Receiver<List<SimpleFooProxy>>() {
       @Override
       public void onSuccess(List<SimpleFooProxy> responseList) {
-        SimpleFooProxy response = responseList.get(0);
+        SimpleFooProxy response = checkSerialization(responseList.get(0));
         assertEquals(42, (int) response.getIntId());
         assertEquals("GWT", response.getUserName());
         assertEquals(8L, (long) response.getLongField());
@@ -576,6 +592,7 @@
 
           @Override
           public void onSuccess(SimpleFooProxy newFoo) {
+            newFoo = checkSerialization(newFoo);
             assertEquals(1, handler.updateEventCount);
             assertEquals(1, handler.totalEventCount);
 
@@ -584,6 +601,7 @@
 
                   @Override
                   public void onSuccess(SimpleFooProxy newFoo) {
+                    newFoo = checkSerialization(newFoo);
                     // no events are fired second time.
                     assertEquals(1, handler.updateEventCount);
                     assertEquals(1, handler.totalEventCount);
@@ -614,6 +632,7 @@
         "selfOneToManyField.selfOneToManyField.fooField").fire(
         new Receiver<SimpleFooProxy>() {
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             assertNotNull(response.getSelfOneToManyField().get(0));
             assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField());
             assertNotNull(response.getSelfOneToManyField().get(0).getSelfOneToManyField().get(
@@ -639,6 +658,7 @@
           public void onSuccess(List<SimpleFooProxy> response) {
             assertEquals(2, response.size());
             for (SimpleFooProxy foo : response) {
+              foo = checkSerialization(foo);
               assertNotNull(foo.stableId());
               assertEquals("FOO", foo.getBarField().getUserName());
             }
@@ -656,6 +676,7 @@
       public void onSuccess(List<SimpleBarProxy> response) {
         assertEquals(2, response.size());
         for (SimpleBarProxy bar : response) {
+          bar = checkSerialization(bar);
           assertNotNull(bar.stableId());
           finishTestAndReset();
         }
@@ -678,7 +699,8 @@
     Request<SimpleBarProxy> fooReq = context.persistAndReturnSelf().using(foo);
     fooReq.fire(new Receiver<SimpleBarProxy>() {
       @Override
-      public void onSuccess(final SimpleBarProxy returned) {
+      public void onSuccess(SimpleBarProxy returned) {
+        returned = checkSerialization(returned);
         EntityProxyId<SimpleBarProxy> persistedId = returned.stableId();
         String persistedToken = req.getHistoryToken(returned.stableId());
 
@@ -703,18 +725,19 @@
       }
     });
   }
-  
+
   /**
    * Make sure our stock RF logging service keeps receiving.
    */
   public void testLoggingService() {
-    String logRecordJson = new StringBuilder("{").append("\"level\": \"ALL\", ")
-      .append("\"loggerName\": \"logger\", ")
-      .append("\"msg\": \"Hi mom\", ")
-      .append("\"timestamp\": \"1234567890\",")
-      .append("\"thrown\": {}")
-      .append("}")
-      .toString();
+    String logRecordJson = new StringBuilder("{") //
+    .append("\"level\": \"ALL\", ") //
+    .append("\"loggerName\": \"logger\", ") //
+    .append("\"msg\": \"Hi mom\", ") //
+    .append("\"timestamp\": \"1234567890\",") //
+    .append("\"thrown\": {}") //
+    .append("}") //
+    .toString();
 
     req.loggingRequest().logMessage(logRecordJson).fire(new Receiver<Void>() {
       @Override
@@ -741,6 +764,7 @@
 
           @Override
           public void onSuccess(SimpleFooProxy newFoo) {
+            newFoo = checkSerialization(newFoo);
             assertEquals(1, handler.updateEventCount);
             assertEquals(1, handler.totalEventCount);
             SimpleFooRequest context = simpleFooRequest();
@@ -762,6 +786,7 @@
                     new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy finalFoo) {
+                        finalFoo = checkSerialization(finalFoo);
                         assertEquals("Ray", finalFoo.getUserName());
                         assertEquals(2, handler.updateEventCount);
                         assertEquals(2, handler.totalEventCount);
@@ -853,8 +878,9 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
-            final Request<Void> fooReq = req.simpleFooRequest().receiveNull(
-                null).using(response);
+            response = checkSerialization(response);
+            Request<Void> fooReq = req.simpleFooRequest().receiveNull(null).using(
+                response);
             fooReq.fire(new Receiver<Void>() {
               @Override
               public void onSuccess(Void v) {
@@ -898,6 +924,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             List<SimpleFooProxy> list = new ArrayList<SimpleFooProxy>();
             list.add(response); // non-null
             list.add(null); // null
@@ -964,6 +991,7 @@
     r.persistAndReturnSelf().using(f).fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy f) {
+        f = checkSerialization(f);
         assertEquals("user name", f.getUserName());
         assertEquals(Byte.valueOf((byte) 100), f.getByteField());
         assertEquals(Short.valueOf((short) 12345), f.getShortField());
@@ -986,12 +1014,14 @@
     simpleBarRequest().findSimpleBarById("999L").fire(
         new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy barProxy) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy barProxy = checkSerialization(response);
             // Retrieve a Foo
             simpleFooRequest().findSimpleFooById(999L).fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy fooProxy) {
+                    fooProxy = checkSerialization(fooProxy);
                     SimpleFooRequest context = simpleFooRequest();
                     fooProxy = context.edit(fooProxy);
                     // Make the Foo point to the Bar
@@ -1002,6 +1032,7 @@
                         "barField").fire(new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy received) {
+                        received = checkSerialization(received);
                         // Check that Foo points to Bar
                         assertNotNull(received.getBarField());
                         assertEquals(barProxy.stableId(),
@@ -1019,6 +1050,7 @@
                             new Receiver<SimpleFooProxy>() {
                               @Override
                               public void onSuccess(SimpleFooProxy response) {
+                                response = checkSerialization(response);
                                 assertNull(response.getBarField());
                                 assertNull(response.getUserName());
                                 assertNull(response.getByteField());
@@ -1048,13 +1080,15 @@
 
     persistRequest.fire(new Receiver<SimpleBarProxy>() {
       @Override
-      public void onSuccess(final SimpleBarProxy persistedBar) {
+      public void onSuccess(SimpleBarProxy response) {
+        final SimpleBarProxy persistedBar = checkSerialization(response);
 
         // It was made, now find a foo to assign it to
         simpleFooRequest().findSimpleFooById(999L).fire(
             new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
 
                 // Found the foo, edit it
                 SimpleFooRequest context = simpleFooRequest();
@@ -1073,6 +1107,7 @@
                           // Here it is
                           @Override
                           public void onSuccess(SimpleFooProxy finalFooProxy) {
+                            finalFooProxy = checkSerialization(finalFooProxy);
                             assertEquals("Amit",
                                 finalFooProxy.getBarField().getUserName());
                             finishTestAndReset();
@@ -1103,6 +1138,7 @@
     fooReq.fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         assertNotNull(response.getBarField());
         assertEquals(newBar.stableId(), response.getBarField().stableId());
         finishTestAndReset();
@@ -1133,6 +1169,7 @@
         new Receiver<SimpleBarProxy>() {
           @Override
           public void onSuccess(SimpleBarProxy response) {
+            response = checkSerialization(response);
             finalFoo.setBarField(response);
             fooReq.fire(new Receiver<Void>() {
               @Override
@@ -1141,6 +1178,7 @@
                     new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy finalFooProxy) {
+                        finalFooProxy = checkSerialization(finalFooProxy);
                         // newFoo hasn't been persisted, so userName is the old
                         // value.
                         assertEquals("GWT", finalFooProxy.getUserName());
@@ -1176,10 +1214,13 @@
 
     fooReq.fire(new Receiver<SimpleFooProxy>() {
       @Override
-      public void onSuccess(final SimpleFooProxy persistedFoo) {
+      public void onSuccess(final SimpleFooProxy response) {
+        final SimpleFooProxy persistedFoo = checkSerialization(response);
         barReq.fire(new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy persistedBar) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy persistedBar = checkSerialization(response);
+
             assertEquals("Ray", persistedFoo.getUserName());
             SimpleFooRequest context = simpleFooRequest();
             final Request<Void> fooReq2 = context.persist().using(persistedFoo);
@@ -1192,6 +1233,7 @@
                     "barField.userName").fire(new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy finalFooProxy) {
+                    finalFooProxy = checkSerialization(finalFooProxy);
                     assertEquals("Amit",
                         finalFooProxy.getBarField().getUserName());
                     finishTestAndReset();
@@ -1215,11 +1257,14 @@
     simpleBarRequest().findSimpleBarById("999L").fire(
         new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy barProxy) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy barProxy = checkSerialization(response);
             simpleFooRequest().findSimpleFooById(999L).with("oneToManyField").fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy fooProxy) {
+                    fooProxy = checkSerialization(fooProxy);
+
                     SimpleFooRequest context = simpleFooRequest();
                     Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                         fooProxy).with("oneToManyField");
@@ -1231,6 +1276,7 @@
                     updReq.fire(new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(response.getOneToManyField().size(),
                             listCount + 1);
                         assertContains(response.getOneToManyField(), barProxy);
@@ -1255,7 +1301,8 @@
     rayFoo.setFooField(rayFoo);
     persistRay.fire(new Receiver<SimpleFooProxy>() {
       @Override
-      public void onSuccess(final SimpleFooProxy persistedRay) {
+      public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         finishTestAndReset();
       }
     });
@@ -1273,7 +1320,8 @@
 
     persistRay.fire(new Receiver<SimpleFooProxy>() {
       @Override
-      public void onSuccess(final SimpleFooProxy persistedRay) {
+      public void onSuccess(SimpleFooProxy response) {
+        final SimpleFooProxy persistedRay = checkSerialization(response);
         SimpleBarRequest context = simpleBarRequest();
         SimpleBarProxy amitBar = context.create(SimpleBarProxy.class);
         final Request<SimpleBarProxy> persistAmit = context.persistAndReturnSelf().using(
@@ -1283,18 +1331,20 @@
 
         persistAmit.fire(new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(SimpleBarProxy persistedAmit) {
+          public void onSuccess(SimpleBarProxy response) {
+            response = checkSerialization(response);
 
             SimpleFooRequest context = simpleFooRequest();
             final Request<SimpleFooProxy> persistRelationship = context.persistAndReturnSelf().using(
                 persistedRay).with("barField");
             SimpleFooProxy newRec = context.edit(persistedRay);
-            newRec.setBarField(persistedAmit);
+            newRec.setBarField(response);
 
             persistRelationship.fire(new Receiver<SimpleFooProxy>() {
               @Override
-              public void onSuccess(SimpleFooProxy relatedRay) {
-                assertEquals("Amit", relatedRay.getBarField().getUserName());
+              public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
+                assertEquals("Amit", response.getBarField().getUserName());
                 finishTestAndReset();
               }
             });
@@ -1311,6 +1361,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy).with("selfOneToManyField");
@@ -1321,6 +1372,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 assertEquals(response.getSelfOneToManyField().size(),
                     listCount + 1);
                 assertContains(response.getSelfOneToManyField(), response);
@@ -1337,6 +1389,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1345,6 +1398,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 assertTrue(response.getNumberListField().contains(100));
                 finishTestAndReset();
               }
@@ -1368,6 +1422,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1377,6 +1432,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 List<Integer> list = response.getNumberListField();
                 assertNull(list);
                 finishTestAndReset();
@@ -1401,6 +1457,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1409,6 +1466,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 assertFalse(response.getNumberListField().contains(oldValue));
                 finishTestAndReset();
               }
@@ -1427,6 +1485,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1439,6 +1498,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 List<Integer> list = response.getNumberListField();
                 assertEquals(5, (int) list.get(0));
                 assertEquals(8, (int) list.get(1));
@@ -1460,6 +1520,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1471,6 +1532,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 Collections.reverse(al);
                 assertTrue(response.getNumberListField().equals(al));
                 finishTestAndReset();
@@ -1490,6 +1552,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy fooProxy) {
+            fooProxy = checkSerialization(fooProxy);
             SimpleFooRequest context = simpleFooRequest();
             Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                 fooProxy);
@@ -1498,6 +1561,7 @@
             updReq.fire(new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 assertTrue(response.getNumberListField().get(0) == 10);
                 finishTestAndReset();
               }
@@ -1518,11 +1582,13 @@
     context.persistAndReturnSelf().using(newBar).fire(
         new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy barProxy) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy barProxy = checkSerialization(response);
             simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy fooProxy) {
+                    fooProxy = checkSerialization(fooProxy);
                     SimpleFooRequest context = simpleFooRequest();
                     Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                         fooProxy).with("oneToManySetField");
@@ -1534,6 +1600,7 @@
                     updReq.fire(new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(listCount + 1,
                             response.getOneToManySetField().size());
                         assertContains(response.getOneToManySetField(),
@@ -1557,11 +1624,14 @@
     simpleBarRequest().findSimpleBarById("1L").fire(
         new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy barProxy) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy barProxy = checkSerialization(response);
+
             simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy fooProxy) {
+                    fooProxy = checkSerialization(fooProxy);
                     SimpleFooRequest context = simpleFooRequest();
                     Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                         fooProxy).with("oneToManySetField");
@@ -1574,6 +1644,7 @@
                     updReq.fire(new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(response.getOneToManySetField().size(),
                             listCount);
                         assertContains(response.getOneToManySetField(),
@@ -1597,11 +1668,13 @@
     simpleBarRequest().findSimpleBarById("1L").fire(
         new Receiver<SimpleBarProxy>() {
           @Override
-          public void onSuccess(final SimpleBarProxy barProxy) {
+          public void onSuccess(SimpleBarProxy response) {
+            final SimpleBarProxy barProxy = checkSerialization(response);
             simpleFooRequest().findSimpleFooById(999L).with("oneToManySetField").fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy fooProxy) {
+                    fooProxy = checkSerialization(fooProxy);
                     SimpleFooRequest context = simpleFooRequest();
                     Request<SimpleFooProxy> updReq = context.persistAndReturnSelf().using(
                         fooProxy).with("oneToManySetField");
@@ -1615,6 +1688,7 @@
                     updReq.fire(new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(listCount - 1,
                             response.getOneToManySetField().size());
                         assertNotContains(response.getOneToManySetField(),
@@ -1650,6 +1724,7 @@
     fooReq.fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         final Request<Integer> sumReq = simpleFooRequest().sum(
             Arrays.asList(1, 2, 3)).using(response);
         sumReq.fire(new Receiver<Integer>() {
@@ -1755,6 +1830,7 @@
     fooReq.fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         assertEquals(2, response.getOneToManyField().size());
 
         // Check lists of proxies returned from a mutable object are mutable
@@ -1771,7 +1847,8 @@
         999L).with("selfOneToManyField");
     fooReq.fire(new Receiver<SimpleFooProxy>() {
       @Override
-      public void onSuccess(final SimpleFooProxy fooProxy) {
+      public void onSuccess(SimpleFooProxy response) {
+        final SimpleFooProxy fooProxy = checkSerialization(response);
         final Request<String> procReq = simpleFooRequest().processList(
             fooProxy.getSelfOneToManyField()).using(fooProxy);
         procReq.fire(new Receiver<String>() {
@@ -1791,6 +1868,7 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
             SimpleFooRequest context = simpleFooRequest();
             SimpleBarProxy bar = context.create(SimpleBarProxy.class);
             Request<String> helloReq = context.hello(bar).using(response);
@@ -1925,7 +2003,8 @@
     fooReq.fire(new Receiver<SimpleFooProxy>() {
 
       @Override
-      public void onSuccess(final SimpleFooProxy returned) {
+      public void onSuccess(SimpleFooProxy response) {
+        final SimpleFooProxy returned = checkSerialization(response);
         assertEquals(futureId, foo.getId());
         assertFalse(((SimpleEntityProxyId<?>) foo.stableId()).isEphemeral());
         assertEquals(futureId, newFoo.getId());
@@ -1943,10 +2022,11 @@
         editRequest.fire(new Receiver<SimpleFooProxy>() {
 
           @Override
-          public void onSuccess(SimpleFooProxy returnedAfterEdit) {
-            assertEquals(returnedAfterEdit.getId(), returned.getId());
-            assertEquals("GWT power user", returnedAfterEdit.getUserName());
-            checkStableIdEquals(editableFoo, returnedAfterEdit);
+          public void onSuccess(SimpleFooProxy response) {
+            response = checkSerialization(response);
+            assertEquals(response.getId(), returned.getId());
+            assertEquals("GWT power user", response.getUserName());
+            checkStableIdEquals(editableFoo, response);
             finishTestAndReset();
           }
         });
@@ -1958,17 +2038,18 @@
    * We provide a simple UserInformation class to give GAE developers a hand,
    * and other developers a hint. Make sure RF doesn't break it (it relies on
    * server side upcasting, and a somewhat sleazey reflective lookup mechanism
-   * in a static method on UserInformation). 
+   * in a static method on UserInformation).
    */
   public void testUserInfo() {
     req.userInformationRequest().getCurrentUserInformation("").fire(
         new Receiver<UserInformationProxy>() {
           @Override
-          public void onSuccess(UserInformationProxy getResponse) {
-            assertEquals("Dummy Email", getResponse.getEmail());
-            assertEquals("Dummy User", getResponse.getName());
-            assertEquals("", getResponse.getLoginUrl());
-            assertEquals("", getResponse.getLogoutUrl());
+          public void onSuccess(UserInformationProxy response) {
+            response = checkSerialization(response);
+            assertEquals("Dummy Email", response.getEmail());
+            assertEquals("Dummy User", response.getName());
+            assertEquals("", response.getLoginUrl());
+            assertEquals("", response.getLogoutUrl());
           }
         });
   }
@@ -1991,11 +2072,13 @@
         new Receiver<SimpleFooProxy>() {
           @Override
           public void onSuccess(SimpleFooProxy response) {
+            // The reconstituted object may not have the same stable id
+            checkStableIdEquals(simpleBar, response.getBarField());
+            response = checkSerialization(response);
             assertEquals(0, handler.totalEventCount);
             checkStableIdEquals(simpleFoo, response);
             SimpleBarProxy responseBar = response.getBarField();
             assertNotNull(responseBar);
-            checkStableIdEquals(simpleBar, responseBar);
             finishTestAndReset();
           }
         });
@@ -2015,6 +2098,7 @@
     context.echo(simpleFoo).fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         assertEquals(0, handler.totalEventCount);
         checkStableIdEquals(simpleFoo, response);
         finishTestAndReset();
@@ -2030,13 +2114,15 @@
     req.simpleFooRequest().getUnpersistedInstance().fire(
         new Receiver<SimpleFooProxy>() {
           @Override
-          public void onSuccess(final SimpleFooProxy created) {
+          public void onSuccess(SimpleFooProxy response) {
+            final SimpleFooProxy created = checkSerialization(response);
             assertNotNull(created);
             assertTrue(created.getUnpersisted());
             req.simpleFooRequest().echo(created).fire(
                 new Receiver<SimpleFooProxy>() {
                   @Override
                   public void onSuccess(SimpleFooProxy response) {
+                    response = checkSerialization(response);
                     assertNotNull(response);
                     assertEquals(created.stableId(), response.stableId());
                     assertTrue(response.getUnpersisted());
@@ -2063,6 +2149,7 @@
         new Receiver<SimpleBarProxy>() {
           @Override
           public void onSuccess(SimpleBarProxy response) {
+            response = checkSerialization(response);
             assertEquals("A", response.getUserName());
             // Mark the object as deleted
             SimpleBarRequest context = simpleBarRequest();
@@ -2074,6 +2161,7 @@
 
                   @Override
                   public void onSuccess(SimpleBarProxy response) {
+                    response = checkSerialization(response);
                     // The last-known state should be returned
                     assertNotNull(response);
                     assertEquals("B", response.getUserName());
@@ -2096,6 +2184,7 @@
 
                           @Override
                           public void onSuccess(SimpleBarProxy response) {
+                            response = checkSerialization(response);
                             fail();
                           }
                         });
@@ -2111,6 +2200,7 @@
     req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         SimpleFooRequest req = simpleFooRequest();
 
         // Create
@@ -2131,6 +2221,7 @@
             new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 SimpleValueProxy value = response.getSimpleValue();
                 assertEquals(42, value.getNumber());
                 assertEquals("Hello world!", value.getString());
@@ -2152,6 +2243,7 @@
                     new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(43, response.getSimpleValue().getNumber());
                         finishTestAndReset();
                       }
@@ -2168,6 +2260,7 @@
     req.findSimpleFooById(1L).fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy response) {
+        response = checkSerialization(response);
         SimpleFooRequest req = simpleFooRequest();
 
         // Create
@@ -2185,6 +2278,7 @@
             new Receiver<SimpleFooProxy>() {
               @Override
               public void onSuccess(SimpleFooProxy response) {
+                response = checkSerialization(response);
                 SimpleValueProxy value = response.getSimpleValues().get(0);
                 assertEquals(42, value.getNumber());
 
@@ -2203,6 +2297,7 @@
                     new Receiver<SimpleFooProxy>() {
                       @Override
                       public void onSuccess(SimpleFooProxy response) {
+                        response = checkSerialization(response);
                         assertEquals(43,
                             response.getSimpleValues().get(0).getNumber());
                         finishTestAndReset();
@@ -2226,6 +2321,18 @@
 
     b.setString("Hello");
     checkEqualityAndHashcode(a, b);
+
+    a.setSimpleValue(Collections.singletonList(req.create(SimpleValueProxy.class)));
+    assertFalse(a.equals(b));
+    assertFalse(b.equals(a));
+    b.setSimpleValue(Collections.singletonList(req.create(SimpleValueProxy.class)));
+    checkEqualityAndHashcode(a, b);
+
+    a.getSimpleValue().get(0).setNumber(55);
+    assertFalse(a.equals(b));
+    assertFalse(b.equals(a));
+    b.getSimpleValue().get(0).setNumber(55);
+    checkEqualityAndHashcode(a, b);
   }
 
   public void testValueObjectViolationsOnCreate() {
@@ -2260,7 +2367,8 @@
     simpleFooRequest().returnValueProxy().fire(
         new Receiver<SimpleValueProxy>() {
           @Override
-          public void onSuccess(final SimpleValueProxy response) {
+          public void onSuccess(SimpleValueProxy response) {
+            final SimpleValueProxy original = checkSerialization(response);
             SimpleFooRequest req = simpleFooRequest();
             final SimpleValueProxy value = req.edit(response);
             value.setShouldBeNull("Hello world");
@@ -2277,7 +2385,7 @@
                 assertEquals(1, errors.size());
                 Violation v = errors.iterator().next();
                 assertEquals(value, v.getInvalidProxy());
-                assertEquals(response, v.getOriginalProxy());
+                assertEquals(original, v.getOriginalProxy());
                 assertEquals("shouldBeNull", v.getPath());
                 assertNull(v.getProxyId());
                 finishTestAndReset();
@@ -2297,6 +2405,7 @@
         new Receiver<SimpleValueProxy>() {
           @Override
           public void onSuccess(SimpleValueProxy a) {
+            a = checkSerialization(a);
             try {
               a.setNumber(77);
               fail();
@@ -2315,6 +2424,7 @@
             ctx.returnValueProxy().fire(new Receiver<SimpleValueProxy>() {
               @Override
               public void onSuccess(SimpleValueProxy b) {
+                b = checkSerialization(b);
                 b = simpleFooRequest().edit(b);
                 // Now check that same value is equal across contexts
                 b.setNumber(77);
@@ -2370,6 +2480,7 @@
     fooCreationRequest().fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy returned) {
+        returned = checkSerialization(returned);
         SimpleFooRequest context = simpleFooRequest();
         Request<SimpleFooProxy> editRequest = context.persistAndReturnSelf().using(
             returned);
@@ -2384,6 +2495,7 @@
     fooCreationRequest().fire(new Receiver<SimpleFooProxy>() {
       @Override
       public void onSuccess(SimpleFooProxy returned) {
+        returned = checkSerialization(returned);
         SimpleFooRequest context = simpleFooRequest();
         Request<Void> editRequest = context.persist().using(returned);
         new FailFixAndRefire<Void>(returned, context, editRequest).doVoidTest();
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java
index ef88d8b..607e4a8 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTestBase.java
@@ -15,14 +15,22 @@
  */
 package com.google.gwt.requestfactory.client;
 
+import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBeanUtils;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.SimpleEventBus;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.requestfactory.shared.BaseProxy;
+import com.google.gwt.requestfactory.shared.DefaultProxyStore;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.EntityProxyChange;
+import com.google.gwt.requestfactory.shared.ProxySerializer;
 import com.google.gwt.requestfactory.shared.Receiver;
 import com.google.gwt.requestfactory.shared.SimpleRequestFactory;
+import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
+import com.google.gwt.requestfactory.shared.impl.Constants;
+import com.google.gwt.requestfactory.shared.impl.SimpleProxyId;
 
 /**
  * A base class for anything that makes use of the SimpleRequestFactory.
@@ -77,6 +85,44 @@
     assertEquals(b, a);
   }
 
+  /**
+   * Run the given proxy through a ProxySerializer and verify that the
+   * before-and-after values match.
+   */
+  protected <T extends BaseProxy> T checkSerialization(T proxy) {
+    AutoBean<T> originalBean = AutoBeanUtils.getAutoBean(proxy);
+    SimpleProxyId<T> id = BaseProxyCategory.stableId(originalBean);
+    DefaultProxyStore store = new DefaultProxyStore();
+    ProxySerializer s = req.getSerializer(store);
+
+    String key = s.serialize(proxy);
+    assertNotNull(key);
+
+    // Use a new instance
+    store = new DefaultProxyStore(store.encode());
+    s = req.getSerializer(store);
+    T restored = s.deserialize(id.getProxyClass(), key);
+    AutoBean<BaseProxy> restoredBean = AutoBeanUtils.getAutoBean(restored);
+    assertNotSame(proxy, restored);
+    /*
+     * Performing a regular assertEquals() or even an AutoBeanUtils.diff() here
+     * is wrong. If any of the objects in the graph are unpersisted, it's
+     * expected that the stable ids would change. Instead, we do a value-based
+     * check.
+     */
+    assertTrue(AutoBeanUtils.deepEquals(originalBean, restoredBean));
+
+    if (proxy instanceof EntityProxy && !id.isEphemeral()) {
+      assertEquals(((EntityProxy) proxy).stableId(),
+          ((EntityProxy) restored).stableId());
+    }
+
+    // In deference to testing stable ids, copy the original id into the clone
+    restoredBean.setTag(Constants.STABLE_ID,
+        originalBean.getTag(Constants.STABLE_ID));
+    return restored;
+  }
+
   protected void checkStableIdEquals(EntityProxy expected, EntityProxy actual) {
     assertEquals(expected.stableId(), actual.stableId());
     assertEquals(expected.stableId().hashCode(), actual.stableId().hashCode());