Re-apply the AutoBean JSO lazy reification patch.
Change type-detection and fast serialization path in JsoSplittable to work
around browser variations.
http://gwt-code-reviews.appspot.com/1407802/
Patch by: bobv
Review by: rice, cromwellian


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9964 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
index 11f6998..5eb9763 100644
--- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
+++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -535,7 +535,7 @@
            */
           JClassType implementingType = typeOracle.getSingleJsoImpl(intfMethod.getEnclosingType());
 
-          if (implementingType == null) {
+          if (implementingType == null || implementingType.isAnnotationPresent(GwtScriptOnly.class)) {
             /*
              * This means that there is no concrete implementation of the
              * interface by a JSO. Any implementation that might be created by a
diff --git a/tools/api-checker/config/gwt22_23userApi.conf b/tools/api-checker/config/gwt22_23userApi.conf
index 88cb8f5..7041d0b 100644
--- a/tools/api-checker/config/gwt22_23userApi.conf
+++ b/tools/api-checker/config/gwt22_23userApi.conf
@@ -16,7 +16,9 @@
 :**/tools/**\
 :com/google/gwt/regexp/shared/**\
 :com/google/gwt/autobean/**/impl/**\
+:com/google/gwt/autobean/shared/ValueCodex.java\
 :com/google/gwt/autobean/shared/ValueCodexHelper.java\
+:com/google/gwt/autobean/shared/AutoBeanCodex.java\
 :com/google/gwt/core/client/impl/WeakMapping.java\
 :com/google/gwt/core/ext/**\
 :com/google/gwt/dev/*.java\
diff --git a/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java b/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java
index 0310304..f8f9e3e 100644
--- a/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java
+++ b/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java
@@ -46,7 +46,7 @@
   /**
    * EnumMap support.
    */
-  public <E extends Enum<E>> E getEnum(Class<E> clazz, String token) {
+  public <E extends Enum<?>> E getEnum(Class<E> clazz, String token) {
     maybeInitializeEnumMap();
     List<Enum<?>> list = stringsToEnumsMap.get(token);
     if (list == null) {
diff --git a/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java b/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java
index 73a077a..620c9ec 100644
--- a/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java
+++ b/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java
@@ -18,6 +18,8 @@
 import com.google.gwt.autobean.shared.AutoBeanVisitor.CollectionPropertyContext;
 import com.google.gwt.autobean.shared.AutoBeanVisitor.MapPropertyContext;
 import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
+import com.google.gwt.autobean.shared.AutoBeanVisitor.PropertyContext;
+import com.google.gwt.autobean.shared.impl.AbstractAutoBean;
 import com.google.gwt.core.client.JavaScriptObject;
 
 import java.util.List;
@@ -27,18 +29,20 @@
 /**
  * Provides base methods for generated implementations of PropertyContext.
  */
-public final class ClientPropertyContext implements CollectionPropertyContext, MapPropertyContext {
+public final class ClientPropertyContext implements PropertyContext, CollectionPropertyContext,
+    MapPropertyContext {
 
   /**
    * A reference to an instance setter method.
    */
   public static final class Setter extends JavaScriptObject {
     /**
-     * Create a trivial Setter that calls {@link Map#put(Object, Object)}.
+     * Create a trivial Setter that calls {@link AbstractAutoBean#setProperty()}
+     * .
      */
-    public static native Setter mapSetter(Map<String, Object> values, String key) /*-{
+    public static native Setter beanSetter(AbstractAutoBean<?> bean, String key) /*-{
       return function(value) {
-        values.@java.util.Map::put(*)(key, value);
+        bean.@com.google.gwt.autobean.shared.impl.AbstractAutoBean::setProperty(*)(key, value);
       };
     }-*/;
 
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 6a86993..008f3f9 100644
--- a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
+++ b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
@@ -16,8 +16,11 @@
 package com.google.gwt.autobean.client.impl;
 
 import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.HasSplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
+import com.google.gwt.core.client.GwtScriptOnly;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsonUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -25,91 +28,129 @@
 
 /**
  * Implements the EntityCodex.Splittable interface using a raw JavaScriptObject.
+ * <p>
+ * A string value represented by a JsoSplittable can't use the string object
+ * directly, since {@code String.prototype} is overridden, so instead a
+ * temporary wrapper object is used to encapsulate the string data.
  */
-public final class JsoSplittable extends JavaScriptObject implements Splittable {
+@GwtScriptOnly
+public final class JsoSplittable extends JavaScriptObject implements Splittable, HasSplittable {
+  private static boolean stringifyFastTested;
+  private static boolean stringifyFastResult;
+
+  public static native JsoSplittable create() /*-{
+    return {};
+  }-*/;
+
+  public static Splittable create(boolean value) {
+    return create0(value);
+  }
+
+  public static Splittable create(double value) {
+    return create0(value);
+  }
+
+  public static Splittable create(String value) {
+    return create0(value);
+  }
+
+  public static native JsoSplittable createIndexed() /*-{
+    return [];
+  }-*/;
+
+  public static native JsoSplittable nullValue() /*-{
+    return null;
+  }-*/;
+
+  private static native Splittable create0(boolean object) /*-{
+    return Boolean(object);
+  }-*/;
+
+  private static native Splittable create0(double object) /*-{
+    return Number(object);
+  }-*/;
+
+  private static native Splittable create0(String object) /*-{
+    return {
+      __s : object
+    };
+  }-*/;
+
+  private static native boolean isUnwrappedString(JavaScriptObject obj) /*-{
+    return Object.prototype.toString.call(obj) == '[object String]';
+  }-*/;
+
+  private static boolean stringifyFastSupported() {
+    if (stringifyFastTested) {
+      return stringifyFastResult;
+    }
+    stringifyFastTested = true;
+    return stringifyFastResult = stringifyFastSupported0();
+  }
+
   /**
-   * This type is used because we can't treat Strings as JSOs.
+   * Test that the JSON api is available and that it does not add function
+   * objects to the output. The test for function objects is for old versions of
+   * Safari.
    */
-  public static class StringSplittable implements Splittable {
-    private final String value;
-
-    public StringSplittable(String value) {
-      this.value = value;
-    }
-
-    public String asString() {
-      return value;
-    }
-
-    public Splittable get(int index) {
-      throw new UnsupportedOperationException();
-    }
-
-    public Splittable get(String key) {
-      throw new UnsupportedOperationException();
-    }
-
-    public String getPayload() {
-      return StringQuoter.quote(value);
-    }
-
-    public List<String> getPropertyKeys() {
-      return Collections.emptyList();
-    }
-
-    public boolean isIndexed() {
-      return false;
-    }
-
-    public boolean isKeyed() {
-      return false;
-    }
-
-    public boolean isNull(int index) {
-      throw new UnsupportedOperationException();
-    }
-
-    public boolean isNull(String key) {
-      throw new UnsupportedOperationException();
-    }
-
-    public boolean isString() {
-      return true;
-    }
-
-    public int size() {
-      return 0;
-    }
-  }
-
-  public static Splittable create(Object object) {
-    if (object instanceof String) {
-      return new StringSplittable((String) object);
-    }
-    return create0(object);
-  }
-
-  private static native Splittable create0(Object object) /*-{
-    return object;
+  private static native boolean stringifyFastSupported0() /*-{
+    return $wnd.JSON && $wnd.JSON.stringify && $wnd.JSON.stringify({
+      b : function() {
+      }
+    }) == '{}';
   }-*/;
 
   protected JsoSplittable() {
+  };
+
+  public native boolean asBoolean() /*-{
+    return this == true;
+  }-*/;
+
+  public native double asNumber() /*-{
+    return Number(this);
+  }-*/;
+
+  public void assign(Splittable parent, int index) {
+    if (isString()) {
+      assign0(parent, index, asString());
+    } else {
+      assign0(parent, index, this);
+    }
+  }
+
+  public void assign(Splittable parent, String index) {
+    if (isString()) {
+      assign0(parent, index, asString());
+    } else {
+      assign0(parent, index, this);
+    }
   }
 
   public native String asString() /*-{
-    return String(this);
+    return this.__s;
   }-*/;
 
-  public Splittable get(int index) {
-    return create(get0(index));
+  public Splittable deepCopy() {
+    return StringQuoter.split(getPayload());
   }
 
-  public Splittable get(String key) {
-    return create(get0(key));
+  public JsoSplittable get(int index) {
+    return getRaw(index);
+  }
+
+  public JsoSplittable get(String key) {
+    return getRaw(key);
   }
 
   public String getPayload() {
-    throw new UnsupportedOperationException("Cannot convert JsoSplittable to payload");
+    if (isString()) {
+      return JsonUtils.escapeValue(asString());
+    }
+    if (stringifyFastSupported()) {
+      return stringifyFast();
+    }
+    return stringifySlow();
   }
 
   public List<String> getPropertyKeys() {
@@ -118,12 +159,28 @@
     return Collections.unmodifiableList(toReturn);
   }
 
+  public native Object getReified(String key) /*-{
+    return this.__reified && this.__reified[':' + key];
+  }-*/;
+
+  public Splittable getSplittable() {
+    return this;
+  }
+
+  public native boolean isBoolean() /*-{
+    return Object.prototype.toString.call(this) == '[object Boolean]';
+  }-*/;
+
+  public native boolean isFunction() /*-{
+    return Object.prototype.toString.call(this) == '[object Function]';
+  }-*/;
+
   public native boolean isIndexed() /*-{
-    return this instanceof Array;
+    return Object.prototype.toString.call(this) == '[object Array]';
   }-*/;
 
   public boolean isKeyed() {
-    return !isString() && !isIndexed();
+    return this != NULL && !isString() && !isIndexed() && !isFunction();
   }
 
   public native boolean isNull(int index) /*-{
@@ -134,20 +191,53 @@
     return this[key] == null;
   }-*/;
 
+  public native boolean isNumber() /*-{
+    return Object.prototype.toString.call(this) == '[object Number]';
+  }-*/;
+
+  public native boolean isReified(String key) /*-{
+    return !!(this.__reified && this.__reified.hasOwnProperty(':' + key));
+  }-*/;
+
+  /**
+   * Returns whether or not the current object is a string-carrier.
+   */
   public native boolean isString() /*-{
-    return typeof (this) == 'string' || this instanceof String;
+    return this && this.__s != null;
+  }-*/;
+
+  public native boolean isUndefined(String key) /*-{
+    return this[key] === undefined;
+  }-*/;
+
+  public native void setReified(String key, Object object) /*-{
+    // Use a function object so native JSON.stringify will ignore
+    (this.__reified || (this.__reified = function() {
+    }))[':' + key] = object;
+  }-*/;
+
+  public native void setSize(int size) /*-{
+    this.length = size;
   }-*/;
 
   public native int size() /*-{
     return this.length;
   }-*/;
 
-  private native Object get0(int index) /*-{
-    return Object(this[index]);
+  private native void assign0(Splittable parent, int index, Splittable value) /*-{
+    parent[index] = value;
   }-*/;
 
-  private native Object get0(String key) /*-{
-    return Object(this[key]);
+  private native void assign0(Splittable parent, int index, String value) /*-{
+    parent[index] = value;
+  }-*/;
+
+  private native void assign0(Splittable parent, String index, Splittable value) /*-{
+    parent[index] = value;
+  }-*/;
+
+  private native void assign0(Splittable parent, String index, String value) /*-{
+    parent[index] = value;
   }-*/;
 
   private native void getPropertyKeys0(List<String> list) /*-{
@@ -157,4 +247,97 @@
       }
     }
   }-*/;
+
+  private native JsoSplittable getRaw(int index) /*-{
+    _ = this[index];
+    if (_ == null) {
+      return null;
+    }
+    if (@com.google.gwt.autobean.client.impl.JsoSplittable::isUnwrappedString(*)(_)) {
+      return @com.google.gwt.autobean.client.impl.JsoSplittable::create(Ljava/lang/String;)(_);
+    }
+    return Object(_);
+  }-*/;
+
+  private native JsoSplittable getRaw(String index) /*-{
+    _ = this[index];
+    if (_ == null) {
+      return null;
+    }
+    if (@com.google.gwt.autobean.client.impl.JsoSplittable::isUnwrappedString(*)(_)) {
+      return @com.google.gwt.autobean.client.impl.JsoSplittable::create(Ljava/lang/String;)(_);
+    }
+    return Object(_);
+  }-*/;
+
+  /**
+   * The test for {@code $H} removes the key in the emitted JSON, however making
+   * a similar test for {@code __reified} causes the key to be emitted with an
+   * explicit {@code null} value.
+   */
+  private native String stringifyFast() /*-{
+    return $wnd.JSON.stringify(this, function(key, value) {
+      if (key == "$H") {
+        return;
+      }
+      return value;
+    });
+  }-*/;
+
+  private String stringifySlow() {
+    StringBuilder sb = new StringBuilder();
+    stringifySlow(sb);
+    return sb.toString();
+  }
+
+  private void stringifySlow(StringBuilder sb) {
+    if (this == NULL) {
+      sb.append("null");
+      return;
+    }
+    if (isBoolean()) {
+      sb.append(asBoolean());
+      return;
+    }
+    if (isNumber()) {
+      sb.append(asNumber());
+      return;
+    }
+    if (isString()) {
+      sb.append(JsonUtils.escapeValue(asString()));
+      return;
+    }
+    if (isIndexed()) {
+      sb.append("[");
+      for (int i = 0, j = size(); i < j; i++) {
+        if (i > 0) {
+          sb.append(",");
+        }
+        get(i).stringifySlow(sb);
+      }
+      sb.append("]");
+      return;
+    }
+
+    sb.append("{");
+    boolean needsComma = false;
+    for (String key : getPropertyKeys()) {
+      if (needsComma) {
+        sb.append(",");
+      } else {
+        needsComma = true;
+      }
+      JsoSplittable value = get(key);
+      if (!value.isFunction()) {
+        if ("$H".equals(key)) {
+          // Ignore hashcode
+          continue;
+        }
+        sb.append(JsonUtils.escapeValue(key));
+        sb.append(":");
+        value.stringifySlow(sb);
+      }
+    }
+    sb.append("}");
+  }
 }
diff --git a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
index a07758d..1ec4f78 100644
--- a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
+++ b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
@@ -27,6 +27,7 @@
 import com.google.gwt.autobean.shared.AutoBeanFactory;
 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.autobean.shared.impl.AbstractAutoBean;
 import com.google.gwt.autobean.shared.impl.AbstractAutoBean.OneShotContext;
 import com.google.gwt.core.client.JavaScriptObject;
@@ -185,28 +186,16 @@
           AutoBeanFactory.class.getCanonicalName());
     }
 
-    // Clone constructor
-    // public FooIntfAutoBean(FooIntfoAutoBean toClone, boolean deepClone) {
-    sw.println("public %1$s(%1$s toClone, boolean deep) {", type.getSimpleSourceName());
-    sw.indentln("super(toClone, deep);");
-    sw.println("}");
-
     // Wrapping constructor
     // public FooIntfAutoBean(AutoBeanFactory factory, FooIntfo wrapped) {
     sw.println("public %s(%s factory, %s wrapped) {", type.getSimpleSourceName(),
         AutoBeanFactory.class.getCanonicalName(), type.getPeerType().getQualifiedSourceName());
-    sw.indentln("super(factory, wrapped);");
+    sw.indentln("super(wrapped, factory);");
     sw.println("}");
 
     // public FooIntf as() {return shim;}
     sw.println("public %s as() {return shim;}", type.getPeerType().getQualifiedSourceName());
 
-    // public FooIntfAutoBean clone(boolean deep) {
-    sw.println("public %s clone(boolean deep) {", type.getQualifiedSourceName());
-    // return new FooIntfAutoBean(this, deep);
-    sw.indentln("return new %s(this, deep);", type.getSimpleSourceName());
-    sw.println("}");
-
     // public Class<Intf> getType() {return Intf.class;}
     sw.println("public Class<%1$s> getType() {return %1$s.class;}", ModelUtils.ensureBaseType(
         type.getPeerType()).getQualifiedSourceName());
@@ -230,45 +219,49 @@
     // return new FooIntf() {
     sw.println("return new %s() {", type.getPeerType().getQualifiedSourceName());
     sw.indent();
+    sw.println("private final %s data = %s.this.data;", Splittable.class.getCanonicalName(), type
+        .getQualifiedSourceName());
     for (AutoBeanMethod method : type.getMethods()) {
       JMethod jmethod = method.getMethod();
+      JType returnType = jmethod.getReturnType();
       sw.println("public %s {", getBaseMethodDeclaration(jmethod));
       sw.indent();
       switch (method.getAction()) {
         case GET: {
-          // Must handle de-boxing primitive types
-          JPrimitiveType primitive = jmethod.getReturnType().isPrimitive();
-          if (primitive != null) {
-            // Object toReturn = values.get("foo");
-            sw.println("Object toReturn = values.get(\"%s\");", method.getPropertyName());
-            sw.println("if (toReturn == null) {");
-            // return 0;
-            sw.indentln("return %s;", primitive.getUninitializedFieldExpression());
-            sw.println("} else {");
-            // return (BoxedType) toReturn;
-            sw.indentln("return (%s) toReturn;", primitive.getQualifiedBoxedSourceName());
-            sw.println("}");
+          String castType;
+          if (returnType.isPrimitive() != null) {
+            castType = returnType.isPrimitive().getQualifiedBoxedSourceName();
+            // Boolean toReturn = getOrReify("foo");
+            sw.println("%s toReturn = getOrReify(\"%s\");", castType, method.getPropertyName());
+            // return toReturn == null ? false : toReturn;
+            sw.println("return toReturn == null ? %s : toReturn;", returnType.isPrimitive()
+                .getUninitializedFieldExpression());
+          } else if (returnType.equals(context.getTypeOracle().findType(
+              Splittable.class.getCanonicalName()))) {
+            sw.println("return data.isNull(\"%1$s\") ? null : data.get(\"%1$s\");", method
+                .getPropertyName());
           } else {
-            // return (ReturnType) values.get(\"foo\");
-            sw.println("return (%s) values.get(\"%s\");", ModelUtils
-                .getQualifiedBaseSourceName(jmethod.getReturnType()), method.getPropertyName());
+            // return (ReturnType) values.getOrReify(\"foo\");
+            castType = ModelUtils.getQualifiedBaseSourceName(returnType);
+            sw.println("return (%s) getOrReify(\"%s\");", castType, method.getPropertyName());
           }
         }
           break;
         case SET:
-        case SET_BUILDER:
-          // values.put("foo", parameter);
-          sw.println("values.put(\"%s\", %s);", method.getPropertyName(),
-              jmethod.getParameters()[0].getName());
+        case SET_BUILDER: {
+          JParameter param = jmethod.getParameters()[0];
+          // setProperty("foo", parameter);
+          sw.println("setProperty(\"%s\", %s);", method.getPropertyName(), param.getName());
           if (JBeanMethod.SET_BUILDER.equals(method.getAction())) {
             sw.println("return this;");
           }
           break;
+        }
         case CALL:
           // return com.example.Owner.staticMethod(Outer.this, param,
           // param);
           JMethod staticImpl = method.getStaticImpl();
-          if (!jmethod.getReturnType().equals(JPrimitiveType.VOID)) {
+          if (!returnType.equals(JPrimitiveType.VOID)) {
             sw.print("return ");
           }
           sw.print("%s.%s(%s.this", staticImpl.getEnclosingType().getQualifiedSourceName(),
@@ -506,19 +499,15 @@
       sw.println("public %s {", getBaseMethodDeclaration(jmethod));
       sw.indent();
 
-      // Use explicit enclosing this reference to avoid method conflicts
-      sw.println("%s.this.checkWrapped();", type.getSimpleSourceName());
-
       switch (method.getAction()) {
         case GET:
           /*
            * The getter call will ensure that any non-value return type is
            * definitely wrapped by an AutoBean instance.
            */
-          // Foo toReturn=FooAutoBean.this.get("getFoo", getWrapped().getFoo());
-          sw.println("%s toReturn = %3$s.this.get(\"%2$s\", getWrapped().%2$s());", ModelUtils
-              .getQualifiedBaseSourceName(jmethod.getReturnType()), methodName, type
-              .getSimpleSourceName());
+          sw.println("%s toReturn = %s.this.getWrapped().%s();", ModelUtils
+              .getQualifiedBaseSourceName(jmethod.getReturnType()), type.getSimpleSourceName(),
+              methodName);
 
           // Non-value types might need to be wrapped
           writeReturnWrapper(sw, type, method);
@@ -526,7 +515,6 @@
           break;
         case SET:
         case SET_BUILDER:
-          sw.println("%s.this.checkFrozen();", type.getSimpleSourceName());
           // getWrapped().setFoo(foo);
           sw.println("%s.this.getWrapped().%s(%s);", type.getSimpleSourceName(), methodName,
               parameters[0].getName());
@@ -596,7 +584,6 @@
     sw.println("%s propertyContext;", ClientPropertyContext.class.getCanonicalName());
     // Local variable ref cleans up emitted js
     sw.println("%1$s as = as();", type.getPeerType().getQualifiedSourceName());
-    sw.println("%s<String, Object> values = this.values;", Map.class.getCanonicalName());
 
     for (AutoBeanMethod method : type.getMethods()) {
       if (!method.getAction().equals(JBeanMethod.GET)) {
@@ -662,9 +649,9 @@
           referencedSetters.add(setter);
         } else {
           // Create a function that will update the values map
-          // CPContext.mapSetter(values, "foo");
-          sw.println("%s.mapSetter(values, \"%s\"),", ClientPropertyContext.Setter.class
-              .getCanonicalName(), method.getPropertyName());
+          // CPContext.beanSetter(FooBeanImpl.this, "foo");
+          sw.println("%s.beanSetter(%s.this, \"%s\"),", ClientPropertyContext.Setter.class
+              .getCanonicalName(), type.getSimpleSourceName(), method.getPropertyName());
         }
       }
       if (typeList.size() == 1) {
diff --git a/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java b/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
index 132393f..32b4a1b 100644
--- a/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
+++ b/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
@@ -16,6 +16,7 @@
 package com.google.gwt.autobean.server.impl;
 
 import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
 
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -38,8 +39,7 @@
     }
 
     @Override
-    Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args)
-        throws Throwable {
+    Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) throws Throwable {
       if (CALL.matches(handler, method)) {
         return CALL.invoke(handler, method, args);
       }
@@ -58,7 +58,7 @@
     @Override
     public String inferName(Method method) {
       String name = method.getName();
-      if (name.startsWith(IS_PREFIX)) {
+      if (name.startsWith(IS_PREFIX) && !method.isAnnotationPresent(PropertyName.class)) {
         Class<?> returnType = method.getReturnType();
         if (Boolean.TYPE.equals(returnType) || Boolean.class.equals(returnType)) {
           return decapitalize(name.substring(2));
@@ -70,7 +70,7 @@
     @Override
     Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) {
       String propertyName = inferName(method);
-      Object toReturn = handler.getBean().getValues().get(propertyName);
+      Object toReturn = handler.getBean().getOrReify(propertyName);
       if (toReturn == null && method.getReturnType().isPrimitive()) {
         toReturn = TypeUtils.getDefaultPrimitiveValue(method.getReturnType());
       }
@@ -80,15 +80,14 @@
     @Override
     boolean matches(SimpleBeanHandler<?> handler, Method method) {
       Class<?> returnType = method.getReturnType();
-      if (method.getParameterTypes().length != 0
-          || Void.TYPE.equals(returnType)) {
+      if (method.getParameterTypes().length != 0 || Void.TYPE.equals(returnType)) {
         return false;
       }
 
       String name = method.getName();
       if (Boolean.TYPE.equals(returnType) || Boolean.class.equals(returnType)) {
-        if (name.startsWith(IS_PREFIX) && name.length() > 2
-            || name.startsWith(HAS_PREFIX) && name.length() > 3) {
+        if (name.startsWith(IS_PREFIX) && name.length() > 2 || name.startsWith(HAS_PREFIX)
+            && name.length() > 3) {
           return true;
         }
       }
@@ -101,7 +100,7 @@
   SET {
     @Override
     Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) {
-      handler.getBean().getValues().put(inferName(method), args[0]);
+      handler.getBean().setProperty(inferName(method), args[0]);
       return null;
     }
 
@@ -109,8 +108,7 @@
     boolean matches(SimpleBeanHandler<?> handler, Method method) {
       String name = method.getName();
       return name.startsWith(SET_PREFIX) && name.length() > 3
-          && method.getParameterTypes().length == 1
-          && method.getReturnType().equals(Void.TYPE);
+          && method.getParameterTypes().length == 1 && method.getReturnType().equals(Void.TYPE);
     }
   },
   /**
@@ -121,15 +119,15 @@
   SET_BUILDER {
     @Override
     Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) {
-      handler.getBean().getValues().put(inferName(method), args[0]);
-      return handler.getBean().as();
+      ProxyAutoBean<?> bean = handler.getBean();
+      bean.setProperty(inferName(method), args[0]);
+      return bean.as();
     }
 
     @Override
     boolean matches(SimpleBeanHandler<?> handler, Method method) {
       String name = method.getName();
-      return name.startsWith(SET_PREFIX)
-          && name.length() > 3
+      return name.startsWith(SET_PREFIX) && name.length() > 3
           && method.getParameterTypes().length == 1
           && method.getReturnType().isAssignableFrom(method.getDeclaringClass());
     }
@@ -144,8 +142,7 @@
     }
 
     @Override
-    Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args)
-        throws Throwable {
+    Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) throws Throwable {
       if (args == null) {
         args = EMPTY_OBJECT;
       }
@@ -190,8 +187,9 @@
           continue;
         }
         // Check the AutoBean parameterization of the 0th argument
-        Class<?> foundAutoBean = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
-            AutoBean.class, found.getGenericParameterTypes()[0]));
+        Class<?> foundAutoBean =
+            TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(AutoBean.class, found
+                .getGenericParameterTypes()[0]));
         if (!foundAutoBean.isAssignableFrom(autoBeanType)) {
           continue;
         }
@@ -205,8 +203,8 @@
   }
 
   /**
-   * Private equivalent of Introspector.decapitalize(String)
-   * since java.beans.Introspector is not available in Android 2.2.
+   * Private equivalent of Introspector.decapitalize(String) since
+   * java.beans.Introspector is not available in Android 2.2.
    */
   private static String decapitalize(String name) {
     if (name == null) {
@@ -223,6 +221,10 @@
   }
 
   public String inferName(Method method) {
+    PropertyName prop = method.getAnnotation(PropertyName.class);
+    if (prop != null) {
+      return prop.value();
+    }
     return decapitalize(method.getName().substring(3));
   }
 
@@ -236,11 +238,11 @@
   /**
    * Invoke the method.
    */
-  abstract Object invoke(SimpleBeanHandler<?> handler, Method method,
-      Object[] args) throws Throwable;
+  abstract Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args)
+      throws Throwable;
 
   /**
    * Determine if the method maches the given type.
    */
   abstract boolean matches(SimpleBeanHandler<?> handler, Method method);
-}
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java b/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java
index f17e879..c1f4be9 100644
--- a/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java
+++ b/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java
@@ -16,20 +16,19 @@
 package com.google.gwt.autobean.server.impl;
 
 import java.lang.reflect.Method;
-import java.util.Map;
 
 /**
  * A property context that allows setters to be called on a simple peer,
  * regardless of whether or not the interface actually has a setter.
  */
 class BeanPropertyContext extends MethodPropertyContext {
+  private final ProxyAutoBean<?> bean;
   private final String propertyName;
-  private final Map<String, Object> map;
 
   public BeanPropertyContext(ProxyAutoBean<?> bean, Method getter) {
     super(getter);
+    this.bean = bean;
     propertyName = BeanMethod.GET.inferName(getter);
-    map = bean.getPropertyMap();
   }
 
   @Override
@@ -42,6 +41,6 @@
     Class<?> maybeAutobox = TypeUtils.maybeAutobox(getType());
     assert value == null || maybeAutobox.isInstance(value) : value.getClass().getCanonicalName()
         + " is not assignable to " + maybeAutobox.getCanonicalName();
-    map.put(propertyName, maybeAutobox.cast(value));
+    bean.setProperty(propertyName, maybeAutobox.cast(value));
   }
 }
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 51e7902..04b77a6 100644
--- a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
+++ b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
@@ -16,21 +16,39 @@
 package com.google.gwt.autobean.server.impl;
 
 import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.HasSplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * Uses the org.json packages to slice and dice request payloads.
  */
-public class JsonSplittable implements Splittable {
+public class JsonSplittable implements Splittable, HasSplittable {
+
+  /**
+   * Ensures that the same JsonSplittable will be returned for a given backing
+   * JSONObject.
+   */
+  private static final Map<Object, Reference<JsonSplittable>> canonical =
+      new WeakHashMap<Object, Reference<JsonSplittable>>();
+
+  public static JsonSplittable create() {
+    return new JsonSplittable(new JSONObject());
+  }
+
   public static Splittable create(String payload) {
     try {
       switch (payload.charAt(0)) {
@@ -39,8 +57,8 @@
         case '[':
           return new JsonSplittable(new JSONArray(payload));
         case '"':
-          return new JsonSplittable(
-              new JSONArray("[" + payload + "]").getString(0));
+          return new JsonSplittable(new JSONArray("[" + payload + "]").getString(0));
+        case '-':
         case '0':
         case '1':
         case '2':
@@ -51,19 +69,31 @@
         case '7':
         case '8':
         case '9':
-          return new JsonSplittable(payload);
+          return new JsonSplittable(Double.parseDouble(payload));
+        case 't':
+        case 'f':
+          return new JsonSplittable(Boolean.parseBoolean(payload));
+        case 'n':
+          return null;
         default:
-          throw new RuntimeException("Could not parse payload: payload[0] = "
-              + payload.charAt(0));
+          throw new RuntimeException("Could not parse payload: payload[0] = " + payload.charAt(0));
       }
     } catch (JSONException e) {
       throw new RuntimeException("Could not parse payload", e);
     }
   }
 
+  public static Splittable createIndexed() {
+    return new JsonSplittable(new JSONArray());
+  }
+
+  public static Splittable createNull() {
+    return new JsonSplittable();
+  }
+
   /**
-   * Private equivalent of org.json.JSONObject.getNames(JSONObject)
-   * since that method is not available in Android 2.2.
+   * Private equivalent of org.json.JSONObject.getNames(JSONObject) since that
+   * method is not available in Android 2.2. Used to represent a null value.
    */
   private static String[] getNames(JSONObject json) {
     int length = json.length();
@@ -79,20 +109,38 @@
     return names;
   }
 
-  private final JSONArray array;
-  private final JSONObject obj;
-  private final String string;
+  private JSONArray array;
+  private Boolean bool;
+  /**
+   * Used to represent a null value.
+   */
+  private boolean isNull;
+  private Double number;
+  private JSONObject obj;
+  private String string;
+  private final Map<String, Object> reified = new HashMap<String, Object>();
+
+  /**
+   * Constructor for a null value.
+   */
+  private JsonSplittable() {
+    isNull = true;
+  }
+
+  private JsonSplittable(boolean value) {
+    this.bool = value;
+  }
+
+  private JsonSplittable(double value) {
+    this.number = value;
+  }
 
   private JsonSplittable(JSONArray array) {
     this.array = array;
-    this.obj = null;
-    this.string = null;
   }
 
   private JsonSplittable(JSONObject obj) {
-    this.array = null;
     this.obj = obj;
-    this.string = null;
   }
 
   private JsonSplittable(String string) {
@@ -101,10 +149,38 @@
     this.string = string;
   }
 
+  public boolean asBoolean() {
+    return bool;
+  }
+
+  public double asNumber() {
+    return number;
+  }
+
+  public void assign(Splittable parent, int index) {
+    try {
+      ((JsonSplittable) parent).array.put(index, value());
+    } catch (JSONException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public void assign(Splittable parent, String propertyName) {
+    try {
+      ((JsonSplittable) parent).obj.put(propertyName, value());
+    } catch (JSONException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   public String asString() {
     return string;
   }
 
+  public Splittable deepCopy() {
+    return create(getPayload());
+  }
+
   public Splittable get(int index) {
     try {
       return makeSplittable(array.get(index));
@@ -122,6 +198,9 @@
   }
 
   public String getPayload() {
+    if (isNull) {
+      return "null";
+    }
     if (obj != null) {
       return obj.toString();
     }
@@ -131,6 +210,12 @@
     if (string != null) {
       return StringQuoter.quote(string);
     }
+    if (number != null) {
+      return String.valueOf(number);
+    }
+    if (bool != null) {
+      return String.valueOf(bool);
+    }
     throw new RuntimeException("No data in this JsonSplittable");
   }
 
@@ -143,6 +228,18 @@
     }
   }
 
+  public Object getReified(String key) {
+    return reified.get(key);
+  }
+
+  public Splittable getSplittable() {
+    return this;
+  }
+
+  public boolean isBoolean() {
+    return bool != null;
+  }
+
   public boolean isIndexed() {
     return array != null;
   }
@@ -160,10 +257,39 @@
     return !obj.has(key) || obj.isNull(key);
   }
 
+  public boolean isNumber() {
+    return number != null;
+  }
+
+  public boolean isReified(String key) {
+    return reified.containsKey(key);
+  }
+
   public boolean isString() {
     return string != null;
   }
 
+  public boolean isUndefined(String key) {
+    return !obj.has(key);
+  }
+
+  public void setReified(String key, Object object) {
+    reified.put(key, object);
+  }
+
+  public void setSize(int size) {
+    // This is terrible, but there's no API support for resizing or splicing
+    JSONArray newArray = new JSONArray();
+    for (int i = 0; i < size; i++) {
+      try {
+        newArray.put(i, array.get(i));
+      } catch (JSONException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    array = newArray;
+  }
+
   public int size() {
     return array.length();
   }
@@ -173,23 +299,53 @@
    */
   @Override
   public String toString() {
-    if (obj != null) {
-      return obj.toString();
-    } else if (array != null) {
-      return array.toString();
-    } else if (string != null) {
-      return string;
-    }
-    return "<Uninitialized>";
+    return getPayload();
   }
 
-  private JsonSplittable makeSplittable(Object object) {
-    if (object instanceof JSONObject) {
-      return new JsonSplittable((JSONObject) object);
+  private synchronized JsonSplittable makeSplittable(Object object) {
+    if (JSONObject.NULL.equals(object)) {
+      return null;
     }
-    if (object instanceof JSONArray) {
-      return new JsonSplittable((JSONArray) object);
+    Reference<JsonSplittable> ref = canonical.get(object);
+    JsonSplittable seen = ref == null ? null : ref.get();
+    if (seen == null) {
+      if (object instanceof JSONObject) {
+        seen = new JsonSplittable((JSONObject) object);
+      } else if (object instanceof JSONArray) {
+        seen = new JsonSplittable((JSONArray) object);
+      } else if (object instanceof String) {
+        seen = new JsonSplittable(object.toString());
+      } else if (object instanceof Number) {
+        seen = new JsonSplittable(((Number) object).doubleValue());
+      } else if (object instanceof Boolean) {
+        seen = new JsonSplittable((Boolean) object);
+      } else {
+        throw new RuntimeException("Unhandled type " + object.getClass());
+      }
+      canonical.put(object, new WeakReference<JsonSplittable>(seen));
     }
-    return new JsonSplittable(object.toString());
+    return seen;
   }
-}
+
+  private Object value() {
+    if (isNull) {
+      return null;
+    }
+    if (obj != null) {
+      return obj;
+    }
+    if (array != null) {
+      return array;
+    }
+    if (string != null) {
+      return string;
+    }
+    if (number != null) {
+      return number;
+    }
+    if (bool != null) {
+      return bool;
+    }
+    throw new RuntimeException("No data");
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java b/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java
index 4f8e3be..de65b5e 100644
--- a/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java
+++ b/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java
@@ -66,15 +66,14 @@
       Class<?>... extraInterfaces) {
     Class<?>[] intfs;
     if (extraInterfaces == null) {
-      intfs = new Class<?>[] {intf};
+      intfs = new Class<?>[]{intf};
     } else {
       intfs = new Class<?>[extraInterfaces.length + 1];
       intfs[0] = intf;
       System.arraycopy(extraInterfaces, 0, intfs, 1, extraInterfaces.length);
     }
 
-    return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs,
-        handler));
+    return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs, handler));
   }
 
   private static Data calculateData(Class<?> beanType) {
@@ -117,13 +116,11 @@
   private final Class<T> beanType;
   private final Configuration configuration;
   private final Data data;
-
   private final T shim;
 
   // These constructors mirror the generated constructors.
   @SuppressWarnings("unchecked")
-  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType,
-      Configuration configuration) {
+  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration) {
     super(factory);
     this.beanType = (Class<T>) beanType;
     this.configuration = configuration;
@@ -132,36 +129,20 @@
   }
 
   @SuppressWarnings("unchecked")
-  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType,
-      Configuration configuration, T toWrap) {
-    super(factory, toWrap);
-    if (Proxy.isProxyClass(toWrap.getClass())) {
-      System.out.println("blah");
-    }
+  public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration,
+      T toWrap) {
+    super(toWrap, factory);
     this.beanType = (Class<T>) beanType;
     this.configuration = configuration;
     this.data = calculateData(beanType);
     this.shim = createShim();
   }
 
-  private ProxyAutoBean(ProxyAutoBean<T> toClone, boolean deep) {
-    super(toClone, deep);
-    this.beanType = toClone.beanType;
-    this.configuration = toClone.configuration;
-    this.data = toClone.data;
-    this.shim = createShim();
-  }
-
   @Override
   public T as() {
     return shim;
   }
 
-  @Override
-  public AutoBean<T> clone(boolean deep) {
-    return new ProxyAutoBean<T>(this, deep);
-  }
-
   public Configuration getConfiguration() {
     return configuration;
   }
@@ -194,9 +175,13 @@
     super.checkWrapped();
   }
 
+  /**
+   * Not used in this implementation. Instead, the simple implementation is
+   * created lazily in {@link #getWrapped()}.
+   */
   @Override
   protected T createSimplePeer() {
-    return ProxyAutoBean.makeProxy(beanType, new SimpleBeanHandler<T>(this));
+    return null;
   }
 
   /**
@@ -208,10 +193,11 @@
   }
 
   /**
-   * Allow access by {@link BeanMethod}.
+   * Allow access by BeanMethod.
    */
-  protected Map<String, Object> getValues() {
-    return values;
+  @Override
+  protected <V> V getOrReify(String propertyName) {
+    return super.<V> getOrReify(propertyName);
   }
 
   /**
@@ -219,6 +205,9 @@
    */
   @Override
   protected T getWrapped() {
+    if (wrapped == null && isUsingSimplePeer()) {
+      wrapped = (T) ProxyAutoBean.makeProxy(beanType, new SimpleBeanHandler<T>(this));
+    }
     return super.getWrapped();
   }
 
@@ -230,6 +219,11 @@
     super.set(method, value);
   }
 
+  @Override
+  protected void setProperty(String propertyName, Object value) {
+    super.setProperty(propertyName, value);
+  }
+
   // TODO: Port to model-based when class-based TypeOracle is available.
   @Override
   protected void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx) {
@@ -257,8 +251,9 @@
       }
 
       // Create the context used for the property visitation
-      MethodPropertyContext x = isUsingSimplePeer() ? new BeanPropertyContext(
-          this, getter) : new GetterPropertyContext(this, getter);
+      MethodPropertyContext x =
+          isUsingSimplePeer() ? new BeanPropertyContext(this, getter) : new GetterPropertyContext(
+              this, getter);
 
       switch (propertyType) {
         case VALUE: {
@@ -313,13 +308,8 @@
     return beanType;
   }
 
-  Map<String, Object> getPropertyMap() {
-    return values;
-  }
-
   private T createShim() {
-    T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this,
-        getWrapped()));
+    T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this, getWrapped()));
     WeakMapping.set(toReturn, AutoBean.class.getName(), this);
     return toReturn;
   }
diff --git a/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java b/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
index 23c190d..dc920a7 100644
--- a/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
+++ b/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
@@ -19,6 +19,7 @@
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 
 import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 
@@ -31,11 +32,9 @@
 class ShimHandler<T> implements InvocationHandler {
   private final ProxyAutoBean<T> bean;
   private final Method interceptor;
-  private final T toWrap;
 
   public ShimHandler(ProxyAutoBean<T> bean, T toWrap) {
     this.bean = bean;
-    this.toWrap = toWrap;
 
     Method maybe = null;
     for (Class<?> clazz : bean.getConfiguration().getCategories()) {
@@ -67,36 +66,35 @@
     return bean.getWrapped().hashCode();
   }
 
-  public Object invoke(Object proxy, Method method, Object[] args)
-      throws Throwable {
+  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     method.setAccessible(true);
     Object toReturn;
     String name = method.getName();
-    bean.checkWrapped();
     method.setAccessible(true);
-    if (BeanMethod.OBJECT.matches(method)) {
-      return method.invoke(this, args);
-    } else if (BeanMethod.GET.matches(method)) {
-      toReturn = method.invoke(toWrap, args);
-      toReturn = bean.get(name, toReturn);
-    } else if (BeanMethod.SET.matches(method)
-        || BeanMethod.SET_BUILDER.matches(method)) {
-      bean.checkFrozen();
-      toReturn = method.invoke(toWrap, args);
-      bean.set(name, args[0]);
-    } else {
-      // XXX How should freezing and calls work together?
-      // bean.checkFrozen();
-      toReturn = method.invoke(toWrap, args);
-      bean.call(name, toReturn, args);
-    }
-    Class<?> intf = method.getReturnType();
-    if (!Object.class.equals(intf)) {
-      // XXX Need to deal with resolving generic T return types
-      toReturn = maybeWrap(intf, toReturn);
-    }
-    if (interceptor != null) {
-      toReturn = interceptor.invoke(null, bean, toReturn);
+    try {
+      if (BeanMethod.OBJECT.matches(method)) {
+        return method.invoke(this, args);
+      } else if (BeanMethod.GET.matches(method)) {
+        toReturn = method.invoke(bean.getWrapped(), args);
+        toReturn = bean.get(name, toReturn);
+      } else if (BeanMethod.SET.matches(method) || BeanMethod.SET_BUILDER.matches(method)) {
+        toReturn = method.invoke(bean.getWrapped(), args);
+        bean.set(name, args[0]);
+      } else {
+        // XXX How should freezing and calls work together?
+        toReturn = method.invoke(bean.getWrapped(), args);
+        bean.call(name, toReturn, args);
+      }
+      Class<?> intf = method.getReturnType();
+      if (!Object.class.equals(intf)) {
+        // XXX Need to deal with resolving generic T return types
+        toReturn = maybeWrap(intf, toReturn);
+      }
+      if (interceptor != null) {
+        toReturn = interceptor.invoke(null, bean, toReturn);
+      }
+    } catch (InvocationTargetException e) {
+      throw e.getCause();
     }
     return toReturn;
   }
@@ -110,9 +108,11 @@
     if (toReturn == null) {
       return null;
     }
-    if (TypeUtils.isValueType(intf)
-        || TypeUtils.isValueType(toReturn.getClass())
-        || AutoBeanUtils.getAutoBean(toReturn) != null
+    AutoBean<?> returnBean = AutoBeanUtils.getAutoBean(toReturn);
+    if (returnBean != null) {
+      return returnBean.as();
+    }
+    if (TypeUtils.isValueType(intf) || TypeUtils.isValueType(toReturn.getClass())
         || bean.getConfiguration().getNoWrap().contains(intf)) {
       return toReturn;
     }
@@ -124,8 +124,8 @@
        */
       return toReturn;
     }
-    ProxyAutoBean<Object> newBean = new ProxyAutoBean<Object>(
-        bean.getFactory(), intf, bean.getConfiguration(), toReturn);
+    ProxyAutoBean<Object> newBean =
+        new ProxyAutoBean<Object>(bean.getFactory(), intf, bean.getConfiguration(), toReturn);
     return newBean.as();
   }
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java b/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java
index d25468d..766d26b 100644
--- a/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java
+++ b/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java
@@ -37,8 +37,7 @@
   /**
    * Delegates most work to {@link BeanMethod}.
    */
-  public Object invoke(Object proxy, Method method, Object[] args)
-      throws Throwable {
+  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
     for (BeanMethod type : BeanMethod.values()) {
       if (type.matches(this, method)) {
         Object toReturn = type.invoke(this, method, args);
@@ -47,12 +46,12 @@
     }
     throw new RuntimeException("Unhandled invocation " + method.getName());
   }
-
+  
   /**
    * For debugging use only.
    */
   @Override
   public String toString() {
-    return bean.getPropertyMap().toString();
+    return bean.getSplittable().getPayload();
   }
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java
index cf769c1..2503696 100644
--- a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java
+++ b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java
@@ -164,8 +164,9 @@
     return false;
   }
 
-  public static Class<?> maybeAutobox(Class<?> domainType) {
-    Class<?> autoBoxType = AUTOBOX_MAP.get(domainType);
+  public static <V> Class<V> maybeAutobox(Class<V> domainType) {
+    @SuppressWarnings("unchecked")
+    Class<V> autoBoxType = (Class<V>) AUTOBOX_MAP.get(domainType);
     return autoBoxType == null ? domainType : autoBoxType;
   }
 
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBean.java b/user/src/com/google/gwt/autobean/shared/AutoBean.java
index 8b8613f..3af5128 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBean.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBean.java
@@ -58,16 +58,21 @@
   T as();
 
   /**
-   * Creates a copy of the AutoBean.
+   * This method always throws an {@link UnsupportedOperationException}. The
+   * implementation of this method in previous releases was not sufficiently
+   * robust and there are no further uses of this method within the GWT code
+   * base. Furthermore, there are many different semantics that can be applied
+   * to a cloning process that cannot be adequately addressed with a single
+   * implementation.
    * <p>
-   * If the AutoBean has tags, the tags will be copied into the cloned AutoBean.
-   * If any of the tag values are AutoBeans, they will not be cloned, regardless
-   * of the value of <code>deep</code>.
+   * A simple clone of an acyclic datastructure can be created by using
+   * {@link AutoBeanCodex} to encode and decode the root object. Other cloning
+   * algorithms are best implemented by using an {@link AutoBeanVisitor}.
    * 
-   * @param deep indicates if all referenced AutoBeans should be cloned
-   * @return a copy of this {@link AutoBean}
-   * @throws IllegalStateException if the AutoBean is a wrapper type
+   * @throws UnsupportedOperationException
+   * @deprecated with no replacement
    */
+  @Deprecated
   AutoBean<T> clone(boolean deep);
 
   /**
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
index b7e4d52..9797984 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -15,21 +15,10 @@
  */
 package com.google.gwt.autobean.shared;
 
-import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
-import com.google.gwt.autobean.shared.impl.EnumMap;
-import com.google.gwt.autobean.shared.impl.LazySplittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.Stack;
-
 /**
  * Utility methods for encoding an AutoBean graph into a JSON-compatible string.
  * This codex intentionally does not preserve object identity, nor does it
@@ -38,363 +27,16 @@
 public class AutoBeanCodex {
 
   /**
-   * Describes a means of encoding or decoding a particular type of data to or
-   * from a wire format representation.
+   * Decode an AutoBeanCodex payload.
+   * 
+   * @param <T> the expected return type
+   * @param factory an AutoBeanFactory capable of producing {@code AutoBean<T>}
+   * @param clazz the expected return type
+   * @param data a payload previously generated by {@link #encode(AutoBean)}
+   * @return an AutoBean containing the payload contents
    */
-  interface Coder {
-    Object decode(Splittable data);
-
-    void encode(StringBuilder sb, Object value);
-  }
-
-  /**
-   * Creates a Coder that is capable of operating on a particular
-   * parameterization of a datastructure (e.g. {@code Map<String, List<String>>}
-   * ).
-   */
-  class CoderCreator extends ParameterizationVisitor {
-    private Stack<Coder> stack = new Stack<Coder>();
-
-    @Override
-    public void endVisitType(Class<?> type) {
-      if (List.class.equals(type) || Set.class.equals(type)) {
-        stack.push(new CollectionCoder(type, stack.pop()));
-      } else if (Map.class.equals(type)) {
-        // Note that the parameters are passed in reverse order
-        stack.push(new MapCoder(stack.pop(), stack.pop()));
-      } else if (Splittable.class.equals(type)) {
-        stack.push(new SplittableDecoder());
-      } else if (type.getEnumConstants() != null) {
-        @SuppressWarnings(value = {"rawtypes", "unchecked"})
-        EnumCoder decoder = new EnumCoder(type);
-        stack.push(decoder);
-      } else if (ValueCodex.canDecode(type)) {
-        stack.push(new ValueCoder(type));
-      } else {
-        stack.push(new ObjectCoder(type));
-      }
-    }
-
-    public Coder getCoder() {
-      assert stack.size() == 1 : "Incorrect size: " + stack.size();
-      return stack.pop();
-    }
-  }
-
-  class CollectionCoder implements Coder {
-    private final Coder elementDecoder;
-    private final Class<?> type;
-
-    public CollectionCoder(Class<?> type, Coder elementDecoder) {
-      this.elementDecoder = elementDecoder;
-      this.type = type;
-    }
-
-    public Object decode(Splittable data) {
-      Collection<Object> collection;
-      if (List.class.equals(type)) {
-        collection = new ArrayList<Object>();
-      } else if (Set.class.equals(type)) {
-        collection = new HashSet<Object>();
-      } else {
-        // Should not reach here
-        throw new RuntimeException(type.getName());
-      }
-      for (int i = 0, j = data.size(); i < j; i++) {
-        Object element = data.isNull(i) ? null : elementDecoder.decode(data.get(i));
-        collection.add(element);
-      }
-      return collection;
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      if (value == null) {
-        sb.append("null");
-        return;
-      }
-
-      Iterator<?> it = ((Collection<?>) value).iterator();
-      sb.append("[");
-      if (it.hasNext()) {
-        elementDecoder.encode(sb, it.next());
-        while (it.hasNext()) {
-          sb.append(",");
-          elementDecoder.encode(sb, it.next());
-        }
-      }
-      sb.append("]");
-    }
-  }
-
-  class EnumCoder<E extends Enum<E>> implements Coder {
-    private final Class<E> type;
-
-    public EnumCoder(Class<E> type) {
-      this.type = type;
-    }
-
-    public Object decode(Splittable data) {
-      return enumMap.getEnum(type, data.asString());
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      if (value == null) {
-        sb.append("null");
-      }
-      sb.append(StringQuoter.quote(enumMap.getToken((Enum<?>) value)));
-    }
-  }
-
-  /**
-   * Used to stop processing.
-   */
-  static class HaltException extends RuntimeException {
-    public HaltException(RuntimeException cause) {
-      super(cause);
-    }
-
-    @Override
-    public RuntimeException getCause() {
-      return (RuntimeException) super.getCause();
-    }
-  }
-
-  class MapCoder implements Coder {
-    private final Coder keyDecoder;
-    private final Coder valueDecoder;
-
-    /**
-     * Parameters in reversed order to accommodate stack-based setup.
-     */
-    public MapCoder(Coder valueDecoder, Coder keyDecoder) {
-      this.keyDecoder = keyDecoder;
-      this.valueDecoder = valueDecoder;
-    }
-
-    public Object decode(Splittable data) {
-      Map<Object, Object> toReturn = new HashMap<Object, Object>();
-      if (data.isIndexed()) {
-        assert data.size() == 2 : "Wrong data size: " + data.size();
-        Splittable keys = data.get(0);
-        Splittable values = data.get(1);
-        for (int i = 0, j = keys.size(); i < j; i++) {
-          Object key = keys.isNull(i) ? null : keyDecoder.decode(keys.get(i));
-          Object value = values.isNull(i) ? null : valueDecoder.decode(values.get(i));
-          toReturn.put(key, value);
-        }
-      } else {
-        ValueCoder keyValueDecoder = (ValueCoder) keyDecoder;
-        for (String rawKey : data.getPropertyKeys()) {
-          Object key = keyValueDecoder.decode(rawKey);
-          Object value = data.isNull(rawKey) ? null : valueDecoder.decode(data.get(rawKey));
-          toReturn.put(key, value);
-        }
-      }
-      return toReturn;
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      if (value == null) {
-        sb.append("null");
-        return;
-      }
-
-      Map<?, ?> map = (Map<?, ?>) value;
-      boolean isSimpleMap = keyDecoder instanceof ValueCoder;
-      if (isSimpleMap) {
-        boolean first = true;
-        sb.append("{");
-        for (Map.Entry<?, ?> entry : map.entrySet()) {
-          Object mapKey = entry.getKey();
-          if (mapKey == null) {
-            // A null key in a simple map is meaningless
-            continue;
-          }
-          Object mapValue = entry.getValue();
-          if (mapValue == null) {
-            // A null value can be ignored
-            continue;
-          }
-
-          if (first) {
-            first = false;
-          } else {
-            sb.append(",");
-          }
-
-          keyDecoder.encode(sb, mapKey);
-          sb.append(":");
-          valueDecoder.encode(sb, mapValue);
-        }
-        sb.append("}");
-      } else {
-        List<Object> keys = new ArrayList<Object>(map.size());
-        List<Object> values = new ArrayList<Object>(map.size());
-        for (Map.Entry<?, ?> entry : map.entrySet()) {
-          keys.add(entry.getKey());
-          values.add(entry.getValue());
-        }
-        sb.append("[");
-        new CollectionCoder(List.class, keyDecoder).encode(sb, keys);
-        sb.append(",");
-        new CollectionCoder(List.class, valueDecoder).encode(sb, values);
-        sb.append("]");
-      }
-    }
-  }
-
-  class ObjectCoder implements Coder {
-    private final Class<?> type;
-
-    public ObjectCoder(Class<?> type) {
-      this.type = type;
-    }
-
-    public Object decode(Splittable data) {
-      AutoBean<?> bean = doDecode(type, data);
-      return bean == null ? null : bean.as();
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      if (value == null) {
-        sb.append("null");
-        return;
-      }
-      doEncode(sb, AutoBeanUtils.getAutoBean(value));
-    }
-  }
-
-  /**
-   * Extracts properties from a bean and turns them into JSON text.
-   */
-  class PropertyGetter extends AutoBeanVisitor {
-    private boolean first = true;
-    private final StringBuilder sb;
-
-    public PropertyGetter(StringBuilder sb) {
-      this.sb = sb;
-    }
-
-    @Override
-    public void endVisit(AutoBean<?> bean, Context ctx) {
-      sb.append("}");
-      seen.pop();
-    }
-
-    @Override
-    public boolean visit(AutoBean<?> bean, Context ctx) {
-      if (seen.contains(bean)) {
-        throw new HaltException(new UnsupportedOperationException("Cycles not supported"));
-      }
-      seen.push(bean);
-      sb.append("{");
-      return true;
-    }
-
-    @Override
-    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
-        PropertyContext ctx) {
-      if (value != null) {
-        encodeProperty(propertyName, value.as(), ctx);
-      }
-      return false;
-    }
-
-    @Override
-    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
-      if (value != null && !value.equals(ValueCodex.getUninitializedFieldValue(ctx.getType()))) {
-        encodeProperty(propertyName, value, ctx);
-      }
-      return false;
-    }
-
-    private void encodeProperty(String propertyName, Object value, PropertyContext ctx) {
-      CoderCreator pd = new CoderCreator();
-      ctx.accept(pd);
-      Coder decoder = pd.getCoder();
-      if (first) {
-        first = false;
-      } else {
-        sb.append(",");
-      }
-      sb.append(StringQuoter.quote(propertyName));
-      sb.append(":");
-      decoder.encode(sb, value);
-    }
-  }
-
-  /**
-   * Populates beans with data extracted from an evaluated JSON payload.
-   */
-  class PropertySetter extends AutoBeanVisitor {
-    private Splittable data;
-
-    public void decodeInto(Splittable data, AutoBean<?> bean) {
-      this.data = data;
-      bean.accept(this);
-    }
-
-    @Override
-    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
-        PropertyContext ctx) {
-      decodeProperty(propertyName, ctx);
-      return false;
-    }
-
-    @Override
-    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
-      decodeProperty(propertyName, ctx);
-      return false;
-    }
-
-    protected void decodeProperty(String propertyName, PropertyContext ctx) {
-      if (!data.isNull(propertyName)) {
-        CoderCreator pd = new CoderCreator();
-        ctx.accept(pd);
-        Coder decoder = pd.getCoder();
-        Object propertyValue = decoder.decode(data.get(propertyName));
-        ctx.set(propertyValue);
-      }
-    }
-  }
-
-  class SplittableDecoder implements Coder {
-    public Object decode(Splittable data) {
-      return data;
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      if (value == null) {
-        sb.append("null");
-        return;
-      }
-      sb.append(((Splittable) value).getPayload());
-    }
-  }
-
-  class ValueCoder implements Coder {
-    private final Class<?> type;
-
-    public ValueCoder(Class<?> type) {
-      assert type.getEnumConstants() == null : "Should use EnumTypeCodex";
-      this.type = type;
-    }
-
-    public Object decode(Splittable propertyValue) {
-      return decode(propertyValue.asString());
-    }
-
-    public Object decode(String propertyValue) {
-      return ValueCodex.decode(type, propertyValue);
-    }
-
-    public void encode(StringBuilder sb, Object value) {
-      sb.append(ValueCodex.encode(value).getPayload());
-    }
-  }
-
   public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz, Splittable data) {
-    return new AutoBeanCodex(factory).doDecode(clazz, data);
+    return AutoBeanCodexImpl.doDecode(EncodeState.forDecode(factory), clazz, data);
   }
 
   /**
@@ -404,7 +46,8 @@
    * @param factory an AutoBeanFactory capable of producing {@code AutoBean<T>}
    * @param clazz the expected return type
    * @param payload a payload string previously generated by
-   *          {@link #encode(AutoBean)}
+   *          {@link #encode(AutoBean)}{@link Splittable#getPayload()
+   *          .getPayload()}.
    * @return an AutoBean containing the payload contents
    */
   public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz, String payload) {
@@ -420,7 +63,7 @@
    * @param bean the target AutoBean
    */
   public static void decodeInto(Splittable data, AutoBean<?> bean) {
-    new AutoBeanCodex(bean.getFactory()).doDecodeInto(data, bean);
+    AutoBeanCodexImpl.doDecodeInto(EncodeState.forDecode(bean.getFactory()), data, bean);
   }
 
   /**
@@ -432,42 +75,12 @@
    */
   public static Splittable encode(AutoBean<?> bean) {
     if (bean == null) {
-      return LazySplittable.NULL;
+      return Splittable.NULL;
     }
 
     StringBuilder sb = new StringBuilder();
-    new AutoBeanCodex(bean.getFactory()).doEncode(sb, bean);
-    return new LazySplittable(sb.toString());
-  }
-
-  private final EnumMap enumMap;
-  private final AutoBeanFactory factory;
-  private final Stack<AutoBean<?>> seen = new Stack<AutoBean<?>>();
-
-  private AutoBeanCodex(AutoBeanFactory factory) {
-    this.factory = factory;
-    this.enumMap = factory instanceof EnumMap ? (EnumMap) factory : null;
-  }
-
-  <T> AutoBean<T> doDecode(Class<T> clazz, Splittable data) {
-    AutoBean<T> toReturn = factory.create(clazz);
-    if (toReturn == null) {
-      throw new IllegalArgumentException(clazz.getName());
-    }
-    doDecodeInto(data, toReturn);
-    return toReturn;
-  }
-
-  void doDecodeInto(Splittable data, AutoBean<?> bean) {
-    new PropertySetter().decodeInto(data, bean);
-  }
-
-  void doEncode(StringBuilder sb, AutoBean<?> bean) {
-    PropertyGetter e = new PropertyGetter(sb);
-    try {
-      bean.accept(e);
-    } catch (HaltException ex) {
-      throw ex.getCause();
-    }
+    EncodeState state = EncodeState.forEncode(bean.getFactory(), sb);
+    AutoBeanCodexImpl.doEncode(state, bean);
+    return StringQuoter.split(sb.toString());
   }
 }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
index e617ddf..ca8de60 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
@@ -314,8 +314,7 @@
    */
   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())) {
+    if (collection.size() != otherCollection.size()) {
       return false;
     }
 
diff --git a/user/src/com/google/gwt/autobean/shared/Splittable.java b/user/src/com/google/gwt/autobean/shared/Splittable.java
index 65a2286..9011fc7 100644
--- a/user/src/com/google/gwt/autobean/shared/Splittable.java
+++ b/user/src/com/google/gwt/autobean/shared/Splittable.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.autobean.shared;
 
+import com.google.gwt.autobean.shared.impl.StringQuoter;
+
 import java.util.List;
 
 /**
@@ -24,11 +26,41 @@
  */
 public interface Splittable {
   /**
+   * A value that represents {@code null}.
+   */
+  Splittable NULL = StringQuoter.nullValue();
+
+  /**
+   * Returns a boolean representation of the data;
+   */
+  boolean asBoolean();
+
+  /**
+   * Returns a numeric representation of the data.
+   */
+  double asNumber();
+
+  /**
+   * Assign the splittable to the specified index of the {@code parent} object.
+   */
+  void assign(Splittable parent, int index);
+
+  /**
+   * Assign the splittable to the named property of the {@code parent} object.
+   */
+  void assign(Splittable parent, String propertyName);
+
+  /**
    * Returns a string representation of the data.
    */
   String asString();
 
   /**
+   * Clones the Splittable, ignoring cycles and tags.
+   */
+  Splittable deepCopy();
+
+  /**
    * Returns the nth element of a list.
    */
   Splittable get(int index);
@@ -50,6 +82,16 @@
   List<String> getPropertyKeys();
 
   /**
+   * Returns a value previously set with {@link #setReified(String, Object)}.
+   */
+  Object getReified(String key);
+
+  /**
+   * Returns {@code true} if the value of the Splittable is a boolean.
+   */
+  boolean isBoolean();
+
+  /**
    * Returns {@code} true if {@link #size()} and {@link #get(int)} can be
    * expected to return meaningful values.
    */
@@ -62,23 +104,49 @@
   boolean isKeyed();
 
   /**
-   * Indicates if the nth element of a list is null.
+   * Indicates if the nth element of a list is null or undefined.
    */
   boolean isNull(int index);
 
   /**
-   * Indicates if the named property is null.
+   * Indicates if the named property is null or undefined.
    */
   boolean isNull(String key);
 
   /**
+   * Returns {@code true} if the value of the Splittable is numeric.
+   */
+  boolean isNumber();
+
+  /**
+   * Returns {@code true} if {@link #setReified(String, Object)} has been called
+   * with the given key.
+   */
+  boolean isReified(String key);
+
+  /**
    * Returns {@code} true if {@link #asString()} can be expected to return a
    * meaningful value.
    */
   boolean isString();
 
   /**
-   * Returns the size of the list.
+   * Returns {@code true} if the value of the key is undefined.
+   */
+  boolean isUndefined(String key);
+
+  /**
+   * Associates a tag value with the Splittable.
+   */
+  void setReified(String key, Object object);
+
+  /**
+   * Resets the length of an indexed Splittable.
+   */
+  void setSize(int i);
+
+  /**
+   * Returns the size of an indexed Splittable.
    */
   int size();
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/shared/ValueCodex.java b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
index b5e103d..8234ee6 100644
--- a/user/src/com/google/gwt/autobean/shared/ValueCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.autobean.shared;
 
-import com.google.gwt.autobean.shared.impl.LazySplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
 
 import java.math.BigDecimal;
@@ -38,13 +37,13 @@
       }
 
       @Override
-      public BigDecimal decode(Class<?> clazz, String value) {
-        return new BigDecimal(value);
+      public BigDecimal decode(Class<?> clazz, Splittable value) {
+        return new BigDecimal(value.asString());
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        return StringQuoter.quote(((BigDecimal) value).toString());
+      public Splittable encode(Object value) {
+        return StringQuoter.create(((BigDecimal) value).toString());
       }
     },
     BIG_INTEGER(BigInteger.class) {
@@ -54,31 +53,46 @@
       }
 
       @Override
-      public BigInteger decode(Class<?> clazz, String value) {
-        return new BigInteger(value);
+      public BigInteger decode(Class<?> clazz, Splittable value) {
+        return new BigInteger(value.asString());
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        return StringQuoter.quote(((BigInteger) value).toString());
+      public Splittable encode(Object value) {
+        return StringQuoter.create(((BigInteger) value).toString());
       }
     },
     BOOLEAN(Boolean.class, boolean.class, false) {
       @Override
-      public Boolean decode(Class<?> clazz, String value) {
-        return Boolean.valueOf(value);
+      public Boolean decode(Class<?> clazz, Splittable value) {
+        return value.asBoolean();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Boolean) value);
       }
     },
     BYTE(Byte.class, byte.class, (byte) 0) {
       @Override
-      public Byte decode(Class<?> clazz, String value) {
-        return Byte.valueOf(value);
+      public Byte decode(Class<?> clazz, Splittable value) {
+        return (byte) value.asNumber();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Byte) value);
       }
     },
     CHARACTER(Character.class, char.class, (char) 0) {
       @Override
-      public Character decode(Class<?> clazz, String value) {
-        return value.charAt(0);
+      public Character decode(Class<?> clazz, Splittable value) {
+        return value.asString().charAt(0);
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create(String.valueOf((Character) value));
       }
     },
     DATE(Date.class) {
@@ -88,80 +102,111 @@
       }
 
       @Override
-      public Date decode(Class<?> clazz, String value) {
-        return StringQuoter.tryParseDate(value);
+      public Date decode(Class<?> clazz, Splittable value) {
+        return StringQuoter.tryParseDate(value.asString());
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        return String.valueOf(((Date) value).getTime());
+      public Splittable encode(Object value) {
+        return StringQuoter.create(String.valueOf(((Date) value).getTime()));
       }
     },
     DOUBLE(Double.class, double.class, 0d) {
       @Override
-      public Double decode(Class<?> clazz, String value) {
-        return Double.valueOf(value);
+      public Double decode(Class<?> clazz, Splittable value) {
+        return value.asNumber();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Double) value);
       }
     },
     ENUM(Enum.class) {
       @Override
-      public Enum<?> decode(Class<?> clazz, String value) {
-        return (Enum<?>) clazz.getEnumConstants()[Integer.valueOf(value)];
+      public Enum<?> decode(Class<?> clazz, Splittable value) {
+        return (Enum<?>) clazz.getEnumConstants()[(int) value.asNumber()];
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        return String.valueOf(((Enum<?>) value).ordinal());
+      public Splittable encode(Object value) {
+        return StringQuoter.create(((Enum<?>) value).ordinal());
       }
     },
     FLOAT(Float.class, float.class, 0f) {
       @Override
-      public Float decode(Class<?> clazz, String value) {
-        return Float.valueOf(value);
+      public Float decode(Class<?> clazz, Splittable value) {
+        return (float) value.asNumber();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Float) value);
       }
     },
     INTEGER(Integer.class, int.class, 0) {
       @Override
-      public Integer decode(Class<?> clazz, String value) {
-        return Integer.valueOf(value);
+      public Integer decode(Class<?> clazz, Splittable value) {
+        return Integer.valueOf((int) value.asNumber());
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Integer) value);
       }
     },
     LONG(Long.class, long.class, 0L) {
       @Override
-      public Long decode(Class<?> clazz, String value) {
-        return Long.valueOf(value);
+      public Long decode(Class<?> clazz, Splittable value) {
+        return Long.parseLong(value.asString());
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        // Longs cannot be expressed as a JS double
-        if (value instanceof Long) {
-          return StringQuoter.quote(String.valueOf(value));
-        } else {
-          throw new IllegalArgumentException("value should be a Long");
-        }
+      public Splittable encode(Object value) {
+        return StringQuoter.create(String.valueOf((Long) value));
       }
     },
     SHORT(Short.class, short.class, (short) 0) {
       @Override
-      public Short decode(Class<?> clazz, String value) {
-        return Short.valueOf(value);
+      public Short decode(Class<?> clazz, Splittable value) {
+        return (short) value.asNumber();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((Short) value);
       }
     },
     STRING(String.class) {
       @Override
-      public String decode(Class<?> clazz, String value) {
+      public String decode(Class<?> clazz, Splittable value) {
+        return value.asString();
+      }
+
+      @Override
+      public Splittable encode(Object value) {
+        return StringQuoter.create((String) value);
+      }
+    },
+    SPLITTABLE(Splittable.class) {
+      @Override
+      public Splittable decode(Class<?> clazz, Splittable value) {
         return value;
       }
 
       @Override
-      public String toJsonExpression(Object value) {
-        return StringQuoter.quote((String) value);
+      public Splittable encode(Object value) {
+        return (Splittable) value;
       }
     },
     VOID(Void.class, void.class, null) {
       @Override
-      public Void decode(Class<?> clazz, String value) {
+      public Void decode(Class<?> clazz, Splittable value) {
+        return null;
+      }
+
+      @Override
+      public Splittable encode(Object value) {
         return null;
       }
     };
@@ -190,7 +235,9 @@
       return false;
     }
 
-    public abstract Object decode(Class<?> clazz, String value);
+    public abstract Object decode(Class<?> clazz, Splittable value);
+
+    public abstract Splittable encode(Object value);
 
     public Object getDefaultValue() {
       return defaultValue;
@@ -203,10 +250,6 @@
     public Class<?> getType() {
       return type;
     }
-
-    public String toJsonExpression(Object value) {
-      return String.valueOf(value);
-    }
   }
 
   private static final Set<Class<?>> ALL_VALUE_TYPES;
@@ -237,19 +280,23 @@
     return ValueCodexHelper.canDecode(clazz);
   }
 
+  @SuppressWarnings("unchecked")
   public static <T> T decode(Class<T> clazz, Splittable split) {
-    if (split == null || split == LazySplittable.NULL) {
+    if (split == null || split == Splittable.NULL) {
       return null;
     }
-    return decode(clazz, split.asString());
+    return (T) getTypeOrDie(clazz).decode(clazz, split);
   }
 
-  @SuppressWarnings("unchecked")
+  /**
+   * No callers in GWT codebase.
+   * 
+   * @deprecated use {@link #decode(Class, Splittable)} instead.
+   * @throws UnsupportedOperationException
+   */
+  @Deprecated
   public static <T> T decode(Class<T> clazz, String string) {
-    if (string == null) {
-      return null;
-    }
-    return (T) getTypeOrDie(clazz).decode(clazz, string);
+    throw new UnsupportedOperationException();
   }
 
   /**
@@ -258,14 +305,14 @@
    */
   public static Splittable encode(Class<?> clazz, Object obj) {
     if (obj == null) {
-      return LazySplittable.NULL;
+      return Splittable.NULL;
     }
-    return new LazySplittable(getTypeOrDie(clazz).toJsonExpression(obj));
+    return getTypeOrDie(clazz).encode(obj);
   }
 
   public static Splittable encode(Object obj) {
     if (obj == null) {
-      return LazySplittable.NULL;
+      return Splittable.NULL;
     }
     Type t = findType(obj.getClass());
     // Try upcasting
@@ -280,7 +327,7 @@
     if (t == null) {
       throw new UnsupportedOperationException(obj.getClass().getName());
     }
-    return new LazySplittable(t.toJsonExpression(obj));
+    return t.encode(obj);
   }
 
   /**
diff --git a/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java b/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java
index ad2dc20..d45da29 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java
@@ -20,6 +20,9 @@
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 import com.google.gwt.autobean.shared.AutoBeanVisitor;
 import com.google.gwt.autobean.shared.AutoBeanVisitor.Context;
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
 import com.google.gwt.core.client.impl.WeakMapping;
 
 import java.util.HashMap;
@@ -32,7 +35,7 @@
  * 
  * @param <T> the wrapper type
  */
-public abstract class AbstractAutoBean<T> implements AutoBean<T> {
+public abstract class AbstractAutoBean<T> implements AutoBean<T>, HasSplittable {
   /**
    * Used to avoid cycles when visiting.
    */
@@ -44,64 +47,50 @@
     }
   }
 
+  public static final String UNSPLITTABLE_VALUES_KEY = "__unsplittableValues";
   protected static final Object[] EMPTY_OBJECT = new Object[0];
 
   /**
    * Used by {@link #createSimplePeer()}.
    */
-  protected final Map<String, Object> values;
-
+  protected Splittable data;
+  protected T wrapped;
   private final AutoBeanFactory factory;
   private boolean frozen;
-
   /**
    * Lazily initialized by {@link #setTag(String, Object)} because not all
    * instances will make use of tags.
    */
   private Map<String, Object> tags;
   private final boolean usingSimplePeer;
-  private T wrapped;
 
   /**
    * Constructor that will use a generated simple peer.
    */
   protected AbstractAutoBean(AutoBeanFactory factory) {
+    this(factory, StringQuoter.createSplittable());
+  }
+
+  /**
+   * Constructor that will use a generated simple peer, backed with existing
+   * data.
+   */
+  protected AbstractAutoBean(AutoBeanFactory factory, Splittable data) {
+    this.data = data;
     this.factory = factory;
     usingSimplePeer = true;
-    values = new HashMap<String, Object>();
+    wrapped = createSimplePeer();
   }
 
   /**
-   * Clone constructor.
+   * Constructor that wraps an existing object. The parameters on this method
+   * are reversed to avoid conflicting with the other two-arg constructor for
+   * {@code AutoBean<Splittable>} instances.
    */
-  protected AbstractAutoBean(AbstractAutoBean<T> toClone, boolean deep) {
-    this.factory = toClone.factory;
-    if (!toClone.usingSimplePeer) {
-      throw new IllegalStateException("Cannot clone wrapped bean");
-    }
-    if (toClone.tags != null) {
-      tags = new HashMap<String, Object>(toClone.tags);
-    }
-    usingSimplePeer = true;
-    values = new HashMap<String, Object>(toClone.values);
-
-    if (deep) {
-      for (Map.Entry<String, Object> entry : values.entrySet()) {
-        AutoBean<?> auto = AutoBeanUtils.getAutoBean(entry.getValue());
-        if (auto != null) {
-          entry.setValue(auto.clone(true).as());
-        }
-      }
-    }
-  }
-
-  /**
-   * Constructor that wraps an existing object.
-   */
-  protected AbstractAutoBean(AutoBeanFactory factory, T wrapped) {
+  protected AbstractAutoBean(T wrapped, AutoBeanFactory factory) {
     this.factory = factory;
     usingSimplePeer = false;
-    values = null;
+    data = null;
     this.wrapped = wrapped;
 
     // Used by AutoBeanUtils
@@ -114,17 +103,31 @@
 
   public abstract T as();
 
-  public abstract AutoBean<T> clone(boolean deep);
+  public AutoBean<T> clone(boolean deep) {
+    throw new UnsupportedOperationException();
+  }
 
   public AutoBeanFactory getFactory() {
     return factory;
   }
 
+  public Splittable getSplittable() {
+    return data;
+  }
+
   @SuppressWarnings("unchecked")
   public <Q> Q getTag(String tagName) {
     return tags == null ? null : (Q) tags.get(tagName);
   }
 
+  /**
+   * Indicates that the value returned from {@link #getSplittable()} may not
+   * contain all of the data encapsulated by the AutoBean.
+   */
+  public boolean hasUnsplittableValues() {
+    return data.isReified(UNSPLITTABLE_VALUES_KEY);
+  }
+
   public boolean isFrozen() {
     return frozen;
   }
@@ -133,6 +136,16 @@
     return !usingSimplePeer;
   }
 
+  public void setData(Splittable data) {
+    assert data != null : "null data";
+    this.data = data;
+    /*
+     * The simple peer aliases the data object from the enclosing bean to avoid
+     * needing to call up the this.this$0 chain.
+     */
+    wrapped = createSimplePeer();
+  }
+
   public void setFrozen(boolean frozen) {
     this.frozen = frozen;
   }
@@ -208,11 +221,30 @@
     return AutoBeanUtils.<W, W> getAutoBean(obj).as();
   }
 
-  protected T getWrapped() {
-    if (wrapped == null) {
-      assert usingSimplePeer : "checkWrapped should have failed";
-      wrapped = createSimplePeer();
+  /**
+   * Native getters and setters for primitive properties are generated for each
+   * type to ensure inlining.
+   */
+  protected <Q> Q getOrReify(String propertyName) {
+    checkWrapped();
+    if (data.isReified(propertyName)) {
+      @SuppressWarnings("unchecked")
+      Q temp = (Q) data.getReified(propertyName);
+      return temp;
     }
+    if (data.isNull(propertyName)) {
+      return null;
+    }
+    data.setReified(propertyName, null);
+    Coder coder = AutoBeanCodexImpl.doCoderFor(this, propertyName);
+    @SuppressWarnings("unchecked")
+    Q toReturn = (Q) coder.decode(EncodeState.forDecode(factory), data.get(propertyName));
+    data.setReified(propertyName, toReturn);
+    return toReturn;
+  }
+
+  protected T getWrapped() {
+    checkWrapped();
     return wrapped;
   }
 
@@ -233,6 +265,27 @@
   protected void set(String method, Object value) {
   }
 
-  protected abstract void traverseProperties(AutoBeanVisitor visitor,
-      OneShotContext ctx);
+  protected void setProperty(String propertyName, Object value) {
+    checkWrapped();
+    checkFrozen();
+    data.setReified(propertyName, value);
+    if (value == null) {
+      Splittable.NULL.assign(data, propertyName);
+      return;
+    }
+    Coder coder = AutoBeanCodexImpl.doCoderFor(this, propertyName);
+    Splittable backing = coder.extractSplittable(EncodeState.forDecode(factory), value);
+    if (backing == null) {
+      /*
+       * External data type, such as an ArrayList or a concrete implementation
+       * of a setter's interface type. This means that a slow serialization pass
+       * is necessary.
+       */
+      data.setReified(UNSPLITTABLE_VALUES_KEY, true);
+    } else {
+      backing.assign(data, propertyName);
+    }
+  }
+
+  protected abstract void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx);
 }
diff --git a/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java b/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java
new file mode 100644
index 0000000..3fbb4d8
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java
@@ -0,0 +1,613 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBeanFactory;
+import com.google.gwt.autobean.shared.AutoBeanUtils;
+import com.google.gwt.autobean.shared.AutoBeanVisitor;
+import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.ValueCodex;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Contains the implementation details of AutoBeanCodex. This type was factored
+ * out of AutoBeanCodex so that various implementation details can be accessed
+ * without polluting a public API.
+ */
+public class AutoBeanCodexImpl {
+
+  /**
+   * Describes a means of encoding or decoding a particular type of data to or
+   * from a wire format representation. Any given instance of a Coder should be
+   * stateless; any state required for operation must be maintained in an
+   * {@link EncodeState}.
+   */
+  public interface Coder {
+    Object decode(EncodeState state, Splittable data);
+
+    void encode(EncodeState state, Object value);
+
+    Splittable extractSplittable(EncodeState state, Object value);
+  }
+
+  /**
+   * Contains transient state for Coder operation.
+   */
+  public static class EncodeState {
+    /**
+     * Constructs a state object used for decoding payloads.
+     */
+    public static EncodeState forDecode(AutoBeanFactory factory) {
+      return new EncodeState(factory, null);
+    }
+
+    /**
+     * Constructs a state object used for encoding payloads.
+     */
+    public static EncodeState forEncode(AutoBeanFactory factory, StringBuilder sb) {
+      return new EncodeState(factory, sb);
+    }
+
+    /**
+     * Constructs a "stateless" state for testing Coders that do not require
+     * AutoBean implementation details.
+     */
+    public static EncodeState forTesting() {
+      return new EncodeState(null, null);
+    }
+
+    final EnumMap enumMap;
+    final AutoBeanFactory factory;
+    final StringBuilder sb;
+    final Stack<AutoBean<?>> seen;
+
+    private EncodeState(AutoBeanFactory factory, StringBuilder sb) {
+      this.factory = factory;
+      enumMap = factory instanceof EnumMap ? (EnumMap) factory : null;
+      this.sb = sb;
+      this.seen = sb == null ? null : new Stack<AutoBean<?>>();
+    }
+  }
+
+  /**
+   * Dynamically creates a Coder that is capable of operating on a particular
+   * parameterization of a datastructure (e.g. {@code Map<String, List<String>>}
+   * ).
+   */
+  static class CoderCreator extends ParameterizationVisitor {
+    private Stack<Coder> stack = new Stack<Coder>();
+
+    @Override
+    public void endVisitType(Class<?> type) {
+      if (List.class.equals(type) || Set.class.equals(type)) {
+        stack.push(collectionCoder(type, stack.pop()));
+      } else if (Map.class.equals(type)) {
+        // Note that the parameters are passed in reverse order
+        stack.push(mapCoder(stack.pop(), stack.pop()));
+      } else if (Splittable.class.equals(type)) {
+        stack.push(splittableCoder());
+      } else if (type.getEnumConstants() != null) {
+        @SuppressWarnings(value = {"unchecked"})
+        Class<Enum<?>> enumType = (Class<Enum<?>>) type;
+        stack.push(enumCoder(enumType));
+      } else if (ValueCodex.canDecode(type)) {
+        stack.push(valueCoder(type));
+      } else {
+        stack.push(objectCoder(type));
+      }
+    }
+
+    public Coder getCoder() {
+      assert stack.size() == 1 : "Incorrect size: " + stack.size();
+      return stack.pop();
+    }
+  }
+
+  /**
+   * Constructs one of the lightweight collection types.
+   */
+  static class CollectionCoder implements Coder {
+    private final Coder elementDecoder;
+    private final Class<?> type;
+
+    public CollectionCoder(Class<?> type, Coder elementDecoder) {
+      this.elementDecoder = elementDecoder;
+      this.type = type;
+    }
+
+    public Object decode(EncodeState state, Splittable data) {
+      Collection<Object> collection;
+      if (List.class.equals(type)) {
+        collection = new SplittableList<Object>(data, elementDecoder, state);
+      } else if (Set.class.equals(type)) {
+        collection = new SplittableSet<Object>(data, elementDecoder, state);
+      } else {
+        // Should not reach here
+        throw new RuntimeException(type.getName());
+      }
+      return collection;
+    }
+
+    public void encode(EncodeState state, Object value) {
+      if (value == null) {
+        state.sb.append("null");
+        return;
+      }
+
+      Iterator<?> it = ((Collection<?>) value).iterator();
+      state.sb.append("[");
+      if (it.hasNext()) {
+        elementDecoder.encode(state, it.next());
+        while (it.hasNext()) {
+          state.sb.append(",");
+          elementDecoder.encode(state, it.next());
+        }
+      }
+      state.sb.append("]");
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return tryExtractSplittable(value);
+    }
+  }
+
+  /**
+   * Produces enums.
+   * 
+   * @param <E>
+   */
+  static class EnumCoder<E extends Enum<?>> implements Coder {
+    private final Class<E> type;
+
+    public EnumCoder(Class<E> type) {
+      this.type = type;
+    }
+
+    public Object decode(EncodeState state, Splittable data) {
+      return state.enumMap.getEnum(type, data.asString());
+    }
+
+    public void encode(EncodeState state, Object value) {
+      if (value == null) {
+        state.sb.append("null");
+        return;
+      }
+      state.sb.append(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value)));
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return StringQuoter.split(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value)));
+    }
+  }
+
+  /**
+   * Used to stop processing.
+   */
+  static class HaltException extends RuntimeException {
+    public HaltException(RuntimeException cause) {
+      super(cause);
+    }
+
+    @Override
+    public RuntimeException getCause() {
+      return (RuntimeException) super.getCause();
+    }
+  }
+
+  /**
+   * Constructs one of the lightweight Map types, depending on the key type.
+   */
+  static class MapCoder implements Coder {
+    private final Coder keyDecoder;
+    private final Coder valueDecoder;
+
+    /**
+     * Parameters in reversed order to accommodate stack-based setup.
+     */
+    public MapCoder(Coder valueDecoder, Coder keyDecoder) {
+      this.keyDecoder = keyDecoder;
+      this.valueDecoder = valueDecoder;
+    }
+
+    public Object decode(EncodeState state, Splittable data) {
+      Map<Object, Object> toReturn;
+      if (data.isIndexed()) {
+        assert data.size() == 2 : "Wrong data size: " + data.size();
+        toReturn = new SplittableComplexMap<Object, Object>(data, keyDecoder, valueDecoder, state);
+      } else {
+        toReturn = new SplittableSimpleMap<Object, Object>(data, keyDecoder, valueDecoder, state);
+      }
+      return toReturn;
+    }
+
+    public void encode(EncodeState state, Object value) {
+      if (value == null) {
+        state.sb.append("null");
+        return;
+      }
+
+      Map<?, ?> map = (Map<?, ?>) value;
+      boolean isSimpleMap = keyDecoder instanceof ValueCoder;
+      if (isSimpleMap) {
+        boolean first = true;
+        state.sb.append("{");
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+          Object mapKey = entry.getKey();
+          if (mapKey == null) {
+            // A null key in a simple map is meaningless
+            continue;
+          }
+          Object mapValue = entry.getValue();
+
+          if (first) {
+            first = false;
+          } else {
+            state.sb.append(",");
+          }
+
+          keyDecoder.encode(state, mapKey);
+          state.sb.append(":");
+          if (mapValue == null) {
+            // Null values must be preserved
+            state.sb.append("null");
+          } else {
+            valueDecoder.encode(state, mapValue);
+          }
+        }
+        state.sb.append("}");
+      } else {
+        List<Object> keys = new ArrayList<Object>(map.size());
+        List<Object> values = new ArrayList<Object>(map.size());
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+          keys.add(entry.getKey());
+          values.add(entry.getValue());
+        }
+        state.sb.append("[");
+        collectionCoder(List.class, keyDecoder).encode(state, keys);
+        state.sb.append(",");
+        collectionCoder(List.class, valueDecoder).encode(state, values);
+        state.sb.append("]");
+      }
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return tryExtractSplittable(value);
+    }
+  }
+
+  /**
+   * Recurses into {@link AutoBeanCodexImpl}.
+   */
+  static class ObjectCoder implements Coder {
+    private final Class<?> type;
+
+    public ObjectCoder(Class<?> type) {
+      this.type = type;
+    }
+
+    public Object decode(EncodeState state, Splittable data) {
+      AutoBean<?> bean = doDecode(state, type, data);
+      return bean == null ? null : bean.as();
+    }
+
+    public void encode(EncodeState state, Object value) {
+      if (value == null) {
+        state.sb.append("null");
+        return;
+      }
+      doEncode(state, AutoBeanUtils.getAutoBean(value));
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return tryExtractSplittable(value);
+    }
+  }
+
+  static class PropertyCoderCreator extends AutoBeanVisitor {
+    private AutoBean<?> bean;
+
+    @Override
+    public boolean visit(AutoBean<?> bean, Context ctx) {
+      this.bean = bean;
+      return true;
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
+        PropertyContext ctx) {
+      maybeCreateCoder(propertyName, ctx);
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
+      maybeCreateCoder(propertyName, ctx);
+      return false;
+    }
+
+    private void maybeCreateCoder(String propertyName, PropertyContext ctx) {
+      CoderCreator creator = new CoderCreator();
+      ctx.accept(creator);
+      coderFor.put(key(bean, propertyName), creator.getCoder());
+    }
+  }
+
+  /**
+   * Extracts properties from a bean and turns them into JSON text.
+   */
+  static class PropertyGetter extends AutoBeanVisitor {
+    private boolean first = true;
+    private final EncodeState state;
+
+    public PropertyGetter(EncodeState state) {
+      this.state = state;
+    }
+
+    @Override
+    public void endVisit(AutoBean<?> bean, Context ctx) {
+      state.sb.append("}");
+      state.seen.pop();
+    }
+
+    @Override
+    public boolean visit(AutoBean<?> bean, Context ctx) {
+      if (state.seen.contains(bean)) {
+        throw new HaltException(new UnsupportedOperationException("Cycles not supported"));
+      }
+      state.seen.push(bean);
+      state.sb.append("{");
+      return true;
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
+        PropertyContext ctx) {
+      if (value != null) {
+        encodeProperty(propertyName, value.as(), ctx);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
+      if (value != null && !value.equals(ValueCodex.getUninitializedFieldValue(ctx.getType()))) {
+        encodeProperty(propertyName, value, ctx);
+      }
+      return false;
+    }
+
+    private void encodeProperty(String propertyName, Object value, PropertyContext ctx) {
+      CoderCreator pd = new CoderCreator();
+      ctx.accept(pd);
+      Coder decoder = pd.getCoder();
+      if (first) {
+        first = false;
+      } else {
+        state.sb.append(",");
+      }
+      state.sb.append(StringQuoter.quote(propertyName));
+      state.sb.append(":");
+      decoder.encode(state, value);
+    }
+  }
+
+  /**
+   * Populates beans with data extracted from an evaluated JSON payload.
+   */
+  static class PropertySetter extends AutoBeanVisitor {
+    private Splittable data;
+    private EncodeState state;
+
+    public void decodeInto(EncodeState state, Splittable data, AutoBean<?> bean) {
+      this.data = data;
+      this.state = state;
+      bean.accept(this);
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
+        PropertyContext ctx) {
+      decodeProperty(propertyName, ctx);
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
+      decodeProperty(propertyName, ctx);
+      return false;
+    }
+
+    protected void decodeProperty(String propertyName, PropertyContext ctx) {
+      if (!data.isNull(propertyName)) {
+        CoderCreator pd = new CoderCreator();
+        ctx.accept(pd);
+        Coder decoder = pd.getCoder();
+        Object propertyValue = decoder.decode(state, data.get(propertyName));
+        ctx.set(propertyValue);
+      }
+    }
+  }
+
+  /**
+   * A passthrough Coder.
+   */
+  static class SplittableCoder implements Coder {
+    static final Coder INSTANCE = new SplittableCoder();
+
+    public Object decode(EncodeState state, Splittable data) {
+      return data;
+    }
+
+    public void encode(EncodeState state, Object value) {
+      if (value == null) {
+        state.sb.append("null");
+        return;
+      }
+      state.sb.append(((Splittable) value).getPayload());
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return (Splittable) value;
+    }
+  }
+
+  /**
+   * Delegates to ValueCodex.
+   */
+  static class ValueCoder implements Coder {
+    private final Class<?> type;
+
+    public ValueCoder(Class<?> type) {
+      assert type.getEnumConstants() == null : "Should use EnumTypeCodex";
+      this.type = type;
+    }
+
+    public Object decode(EncodeState state, Splittable propertyValue) {
+      if (propertyValue == null || propertyValue == Splittable.NULL) {
+        return ValueCodex.getUninitializedFieldValue(type);
+      }
+      return ValueCodex.decode(type, propertyValue);
+    }
+
+    public void encode(EncodeState state, Object value) {
+      state.sb.append(ValueCodex.encode(type, value).getPayload());
+    }
+
+    public Splittable extractSplittable(EncodeState state, Object value) {
+      return ValueCodex.encode(type, value);
+    }
+  }
+
+  /**
+   * A map of AutoBean interface+property names to the Coder for that property.
+   */
+  private static final Map<String, Coder> coderFor = new HashMap<String, Coder>();
+  /**
+   * A map of types to a Coder that handles the type.
+   */
+  private static final Map<Class<?>, Coder> coders = new HashMap<Class<?>, Coder>();
+
+  public static Coder collectionCoder(Class<?> type, Coder elementCoder) {
+    return new CollectionCoder(type, elementCoder);
+  }
+
+  public static Coder doCoderFor(AutoBean<?> bean, String propertyName) {
+    String key = key(bean, propertyName);
+    Coder toReturn = coderFor.get(key);
+    if (toReturn == null) {
+      bean.accept(new PropertyCoderCreator());
+      toReturn = coderFor.get(key);
+      if (toReturn == null) {
+        throw new IllegalArgumentException(propertyName);
+      }
+    }
+    return toReturn;
+  }
+
+  public static <T> AutoBean<T> doDecode(EncodeState state, Class<T> clazz, Splittable data) {
+    /*
+     * If we decode the same Splittable twice, re-use the ProxyAutoBean to
+     * maintain referential integrity. If we didn't do this, either facade would
+     * update the same backing data, yet not be the same object via ==
+     * comparison.
+     */
+    @SuppressWarnings("unchecked")
+    AutoBean<T> toReturn = (AutoBean<T>) data.getReified(AutoBeanCodexImpl.class.getName());
+    if (toReturn != null) {
+      return toReturn;
+    }
+    toReturn = state.factory.create(clazz);
+    data.setReified(AutoBeanCodexImpl.class.getName(), toReturn);
+    if (toReturn == null) {
+      throw new IllegalArgumentException(clazz.getName());
+    }
+    ((AbstractAutoBean<T>) toReturn).setData(data);
+    return toReturn;
+  }
+
+  public static void doDecodeInto(EncodeState state, Splittable data, AutoBean<?> bean) {
+    new PropertySetter().decodeInto(state, data, bean);
+  }
+
+  public static void doEncode(EncodeState state, AutoBean<?> bean) {
+    PropertyGetter e = new PropertyGetter(state);
+    try {
+      bean.accept(e);
+    } catch (HaltException ex) {
+      throw ex.getCause();
+    }
+  }
+
+  public static <E extends Enum<?>> Coder enumCoder(Class<E> type) {
+    Coder toReturn = coders.get(type);
+    if (toReturn == null) {
+      toReturn = new EnumCoder<E>(type);
+      coders.put(type, toReturn);
+    }
+    return toReturn;
+  }
+
+  public static Coder mapCoder(Coder valueCoder, Coder keyCoder) {
+    return new MapCoder(valueCoder, keyCoder);
+  }
+
+  public static Coder objectCoder(Class<?> type) {
+    Coder toReturn = coders.get(type);
+    if (toReturn == null) {
+      toReturn = new ObjectCoder(type);
+      coders.put(type, toReturn);
+    }
+    return toReturn;
+  }
+
+  public static Coder splittableCoder() {
+    return SplittableCoder.INSTANCE;
+  }
+
+  public static Coder valueCoder(Class<?> type) {
+    Coder toReturn = coders.get(type);
+    if (toReturn == null) {
+      toReturn = new ValueCoder(type);
+      coders.put(type, toReturn);
+    }
+    return toReturn;
+  }
+
+  static Splittable tryExtractSplittable(Object value) {
+    AutoBean<?> bean = AutoBeanUtils.getAutoBean(value);
+    if (bean != null) {
+      value = bean;
+    }
+    if (bean instanceof HasSplittable) {
+      return ((HasSplittable) bean).getSplittable();
+    }
+    return null;
+  }
+
+  private static String key(AutoBean<?> bean, String propertyName) {
+    return bean.getType().getName() + ":" + propertyName;
+  }
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java
index 3b53d1b..0a3d298 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java
@@ -27,7 +27,7 @@
     Class<? extends Enum<?>>[] value();
   }
 
-  <E extends Enum<E>> E getEnum(Class<E> clazz, String token);
+  <E extends Enum<?>> E getEnum(Class<E> clazz, String token);
 
   String getToken(Enum<?> e);
 }
diff --git a/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java b/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java
new file mode 100644
index 0000000..7ae4a33
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.Splittable;
+
+/**
+ * Allows reified type facades to return their underlying Splittable
+ * datastructure.
+ */
+public interface HasSplittable {
+  Splittable getSplittable();
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
deleted file mode 100644
index 2fb4749..0000000
--- a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.autobean.shared.impl;
-
-import com.google.gwt.autobean.shared.Splittable;
-
-import java.util.List;
-
-/**
- * Holds a string payload with the expectation that the object will be used only
- * for creating a larger payload.
- */
-public class LazySplittable implements Splittable {
-  public static final Splittable NULL = new LazySplittable("null");
-
-  private final String payload;
-  private Splittable split;
-
-  public LazySplittable(String payload) {
-    this.payload = payload;
-  }
-
-  public String asString() {
-    maybeSplit();
-    return split.asString();
-  }
-
-  public Splittable get(int index) {
-    maybeSplit();
-    return split.get(index);
-  }
-
-  public Splittable get(String key) {
-    maybeSplit();
-    return split.get(key);
-  }
-
-  public String getPayload() {
-    return payload;
-  }
-
-  public List<String> getPropertyKeys() {
-    maybeSplit();
-    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);
-  }
-
-  public boolean isNull(String key) {
-    maybeSplit();
-    return split.isNull(key);
-  }
-
-  public boolean isString() {
-    return payload.charAt(0) == '\"';
-  }
-
-  public int size() {
-    maybeSplit();
-    return split.size();
-  }
-
-  private void maybeSplit() {
-    if (split == null) {
-      split = StringQuoter.split(payload);
-    }
-  }
-}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java
new file mode 100644
index 0000000..4225512
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A Map implementation for complex keys.
+ * 
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class SplittableComplexMap<K, V> implements Map<K, V>, HasSplittable {
+  private final Splittable data;
+  private final List<K> keys;
+  private final List<V> values;
+
+  public SplittableComplexMap(Splittable data, Coder keyCoder, Coder valueCoder, EncodeState state) {
+    this.data = data;
+    this.keys = new SplittableList<K>(data.get(0), keyCoder, state);
+    this.values = new SplittableList<V>(data.get(1), valueCoder, state);
+    assert this.keys.size() == this.values.size();
+  }
+
+  public void clear() {
+    // Trigger ConcurrentModificationExceptions for any outstanding Iterators
+    keys.clear();
+    values.clear();
+  }
+
+  public boolean containsKey(Object key) {
+    return keys.contains(key);
+  }
+
+  public boolean containsValue(Object value) {
+    return values.contains(value);
+  }
+
+  public Set<java.util.Map.Entry<K, V>> entrySet() {
+    return new AbstractSet<Map.Entry<K, V>>() {
+
+      @Override
+      public Iterator<java.util.Map.Entry<K, V>> iterator() {
+        return new Iterator<Map.Entry<K, V>>() {
+          Iterator<K> keyIt = keys.iterator();
+          ListIterator<V> valueIt = values.listIterator();
+
+          public boolean hasNext() {
+            assert keyIt.hasNext() == valueIt.hasNext();
+            return keyIt.hasNext();
+          }
+
+          public java.util.Map.Entry<K, V> next() {
+            return new Map.Entry<K, V>() {
+              final K key = keyIt.next();
+              final V value = valueIt.next();
+
+              public K getKey() {
+                return key;
+              }
+
+              public V getValue() {
+                return value;
+              }
+
+              public V setValue(V value) {
+                valueIt.set(value);
+                return value;
+              }
+            };
+          }
+
+          public void remove() {
+            keyIt.remove();
+            valueIt.remove();
+          }
+        };
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+
+  public V get(Object key) {
+    int idx = keys.indexOf(key);
+    if (idx == -1) {
+      return null;
+    }
+    return values.get(idx);
+  }
+
+  public Splittable getSplittable() {
+    return data;
+  }
+
+  public boolean isEmpty() {
+    return keys.isEmpty();
+  }
+
+  public Set<K> keySet() {
+    return new AbstractSet<K>() {
+      @Override
+      public Iterator<K> iterator() {
+        return keys.iterator();
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+
+  public V put(K key, V value) {
+    int idx = keys.indexOf(key);
+    if (idx == -1) {
+      keys.add(key);
+      values.add(value);
+      return null;
+    }
+    return values.set(idx, value);
+  }
+
+  public void putAll(Map<? extends K, ? extends V> m) {
+    for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
+      put(entry.getKey(), entry.getValue());
+    }
+  }
+
+  public V remove(Object key) {
+    int idx = keys.indexOf(key);
+    if (idx == -1) {
+      return null;
+    }
+    keys.remove(idx);
+    return values.remove(idx);
+  }
+
+  public int size() {
+    return keys.size();
+  }
+
+  public Collection<V> values() {
+    return new AbstractCollection<V>() {
+      @Override
+      public Iterator<V> iterator() {
+        return new Iterator<V>() {
+          final Iterator<K> keyIt = keys.iterator();
+          final Iterator<V> valueIt = values.iterator();
+
+          public boolean hasNext() {
+            return keyIt.hasNext();
+          }
+
+          public V next() {
+            keyIt.next();
+            return valueIt.next();
+          }
+
+          public void remove() {
+            keyIt.remove();
+            valueIt.remove();
+          }
+        };
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java
new file mode 100644
index 0000000..0a7df50
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
+
+import java.util.AbstractList;
+
+/**
+ * A list implementation that lazily reifies its constituent elements.
+ * 
+ * @param <E> the element type
+ */
+public class SplittableList<E> extends AbstractList<E> implements HasSplittable {
+  static <Q> Q reify(EncodeState state, Splittable data, int index, Coder coder) {
+    if (data.isNull(index)) {
+      return null;
+    }
+    @SuppressWarnings("unchecked")
+    Q toReturn = (Q) coder.decode(state, data.get(index));
+    data.setReified(String.valueOf(index), toReturn);
+    return toReturn;
+  }
+
+  static void set(EncodeState state, Splittable data, int index, Coder coder, Object value) {
+    data.setReified(String.valueOf(index), value);
+    if (value == null) {
+      Splittable.NULL.assign(data, index);
+      return;
+    }
+    Splittable backing = coder.extractSplittable(state, value);
+    if (backing == null) {
+      /*
+       * External data type, such as an ArrayList or a concrete implementation
+       * of a setter's interface type. This means that a slow serialization pass
+       * is necessary.
+       */
+      data.setReified(AbstractAutoBean.UNSPLITTABLE_VALUES_KEY, true);
+    } else {
+      backing.assign(data, index);
+    }
+  }
+
+  private Splittable data;
+  private final Coder elementCoder;
+  private final EncodeState state;
+
+  public SplittableList(Splittable data, Coder elementCoder, EncodeState state) {
+    assert data.isIndexed() : "Expecting indexed data";
+    this.data = data;
+    this.elementCoder = elementCoder;
+    this.state = state;
+  }
+
+  @Override
+  public void add(int index, E element) {
+    set(state, data, index, elementCoder, element);
+  }
+
+  @Override
+  public E get(int index) {
+    if (data.isReified(String.valueOf(index))) {
+      @SuppressWarnings("unchecked")
+      E toReturn = (E) data.getReified(String.valueOf(index));
+      return toReturn;
+    }
+    // javac generics bug
+    return SplittableList.<E> reify(state, data, index, elementCoder);
+  }
+
+  public Splittable getSplittable() {
+    return data;
+  }
+
+  @Override
+  public E remove(int index) {
+    E toReturn = get(index);
+    // XXX This is terrible, use Array.splice
+    int newSize = data.size() - 1;
+    for (int i = index; i < newSize; i++) {
+      data.get(i + 1).assign(data, i);
+      data.setReified(String.valueOf(i), data.getReified(String.valueOf(i + 1)));
+    }
+    data.setSize(newSize);
+    return toReturn;
+  }
+
+  @Override
+  public E set(int index, E element) {
+    E previous = get(index);
+    set(state, data, index, elementCoder, element);
+    return previous;
+  }
+
+  @Override
+  public int size() {
+    return data.size();
+  }
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java
new file mode 100644
index 0000000..fbd3f5d
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+
+/**
+ * This type is optimized for the read-only case and has {@code O(n)} insertion
+ * / lookup performance since computing hashcodes for the elements would require
+ * up-front reification.
+ * 
+ * @param <E> the element type
+ */
+public class SplittableSet<E> extends AbstractSet<E> implements HasSplittable {
+  private SplittableList<E> data;
+
+  public SplittableSet(Splittable data, Coder elementCoder, EncodeState state) {
+    this.data = new SplittableList<E>(data, elementCoder, state);
+  }
+
+  @Override
+  public boolean add(E e) {
+    if (!data.contains(e)) {
+      data.add(e);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void clear() {
+    data.clear();
+  }
+
+  public Splittable getSplittable() {
+    return data.getSplittable();
+  }
+
+  @Override
+  public Iterator<E> iterator() {
+    return data.iterator();
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    return data.remove(o);
+  }
+
+  @Override
+  public int size() {
+    return data.size();
+  }
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java
new file mode 100644
index 0000000..edfeff4
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
+
+import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A Map implementation for regular JSON maps with value-type keys.
+ * 
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+public class SplittableSimpleMap<K, V> implements Map<K, V>, HasSplittable {
+  private final Splittable data;
+  private final Coder keyCoder;
+  private final EncodeState state;
+  private final Coder valueCoder;
+  /**
+   * Don't hang the reified data from {@link #data} since we can't tell the
+   * __reified field from the actual data.
+   */
+  private Splittable reified = StringQuoter.createSplittable();
+
+  public SplittableSimpleMap(Splittable data, Coder keyCoder, Coder valueCoder, EncodeState state) {
+    this.data = data;
+    this.keyCoder = keyCoder;
+    this.state = state;
+    this.valueCoder = valueCoder;
+  }
+
+  public void clear() {
+    for (String key : data.getPropertyKeys()) {
+      Splittable.NULL.assign(data, key);
+      reified.setReified(key, null);
+    }
+  }
+
+  public boolean containsKey(Object key) {
+    String encodedKey = encodedKey(key);
+    return !data.isUndefined(encodedKey) || reified.isReified(encodedKey);
+  }
+
+  public boolean containsValue(Object value) {
+    return values().contains(value);
+  }
+
+  public Set<java.util.Map.Entry<K, V>> entrySet() {
+    return new AbstractSet<Map.Entry<K, V>>() {
+      final List<String> keys = data.getPropertyKeys();
+
+      @Override
+      public Iterator<java.util.Map.Entry<K, V>> iterator() {
+        return new Iterator<Map.Entry<K, V>>() {
+          Iterator<String> keyIterator = keys.iterator();
+          String encodedKey;
+
+          public boolean hasNext() {
+            return keyIterator.hasNext();
+          }
+
+          public java.util.Map.Entry<K, V> next() {
+            encodedKey = keyIterator.next();
+            return new Map.Entry<K, V>() {
+              @SuppressWarnings("unchecked")
+              final K key = (K) keyCoder.decode(state, StringQuoter.split(StringQuoter
+                  .quote(encodedKey)));
+              @SuppressWarnings("unchecked")
+              final V value = (V) valueCoder.decode(state, data.get(encodedKey));
+
+              public K getKey() {
+                return key;
+              }
+
+              public V getValue() {
+                return value;
+              }
+
+              public V setValue(V newValue) {
+                return put(key, newValue);
+              }
+            };
+          }
+
+          public void remove() {
+            Splittable.NULL.assign(data, encodedKey);
+            reified.setReified(encodedKey, null);
+          }
+        };
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+
+  public V get(Object key) {
+    String encodedKey = encodedKey(key);
+    return getRaw(encodedKey);
+  }
+
+  public Splittable getSplittable() {
+    return data;
+  }
+
+  public boolean isEmpty() {
+    return data.getPropertyKeys().isEmpty();
+  }
+
+  public Set<K> keySet() {
+    return new AbstractSet<K>() {
+      final List<String> keys = data.getPropertyKeys();
+
+      @Override
+      public Iterator<K> iterator() {
+        return new Iterator<K>() {
+          final Iterator<String> it = keys.iterator();
+          String lastEncodedKey;
+
+          public boolean hasNext() {
+            return it.hasNext();
+          }
+
+          public K next() {
+            lastEncodedKey = it.next();
+            @SuppressWarnings("unchecked")
+            K toReturn =
+                (K) keyCoder.decode(state, StringQuoter.split(StringQuoter.quote(lastEncodedKey)));
+            return toReturn;
+          }
+
+          public void remove() {
+            Splittable.NULL.assign(data, lastEncodedKey);
+            reified.setReified(lastEncodedKey, null);
+          }
+        };
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+
+  public V put(K key, V value) {
+    V toReturn = get(key);
+    String encodedKey = encodedKey(key);
+    reified.setReified(encodedKey, value);
+    Splittable encodedValue = valueCoder.extractSplittable(state, value);
+    if (encodedValue == null) {
+      // External datastructure
+      reified.setReified(AbstractAutoBean.UNSPLITTABLE_VALUES_KEY, true);
+    } else {
+      encodedValue.assign(data, encodedKey);
+    }
+    return toReturn;
+  }
+
+  public void putAll(Map<? extends K, ? extends V> m) {
+    for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
+      put(entry.getKey(), entry.getValue());
+    }
+  }
+
+  public V remove(Object key) {
+    V toReturn = get(key);
+    String encodedKey = encodedKey(key);
+    reified.setReified(encodedKey, null);
+    Splittable.NULL.assign(data, encodedKey);
+    return toReturn;
+  }
+
+  public int size() {
+    return data.getPropertyKeys().size();
+  }
+
+  public Collection<V> values() {
+    return new AbstractCollection<V>() {
+      final List<String> keys = data.getPropertyKeys();
+
+      @Override
+      public Iterator<V> iterator() {
+        return new Iterator<V>() {
+          final Iterator<String> it = keys.iterator();
+          String lastEncodedKey;
+
+          public boolean hasNext() {
+            return it.hasNext();
+          }
+
+          public V next() {
+            lastEncodedKey = it.next();
+            return getRaw(lastEncodedKey);
+          }
+
+          public void remove() {
+            Splittable.NULL.assign(data, lastEncodedKey);
+            reified.setReified(lastEncodedKey, null);
+          }
+        };
+      }
+
+      @Override
+      public int size() {
+        return keys.size();
+      }
+    };
+  }
+
+  private String encodedKey(Object key) {
+    return keyCoder.extractSplittable(state, key).asString();
+  }
+
+  private V getRaw(String encodedKey) {
+    if (reified.isReified(encodedKey)) {
+      @SuppressWarnings("unchecked")
+      V toReturn = (V) reified.getReified(encodedKey);
+      return toReturn;
+    }
+    // Both undefined or an explicit null should return null here
+    if (data.isNull(encodedKey)) {
+      return null;
+    }
+    Splittable value = data.get(encodedKey);
+    @SuppressWarnings("unchecked")
+    V toReturn = (V) valueCoder.decode(state, value);
+    reified.setReified(encodedKey, toReturn);
+    return toReturn;
+  }
+}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
index 899ed52..559e2af 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -31,12 +31,36 @@
  */
 public class StringQuoter {
   private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSz";
-  private static final DateFormat ISO8601 = new SimpleDateFormat(
-      ISO8601_PATTERN, Locale.getDefault());
+  private static final DateFormat ISO8601 = new SimpleDateFormat(ISO8601_PATTERN, Locale
+      .getDefault());
 
   private static final String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z";
-  private static final DateFormat RFC2822 = new SimpleDateFormat(
-      RFC2822_PATTERN, Locale.getDefault());
+  private static final DateFormat RFC2822 = new SimpleDateFormat(RFC2822_PATTERN, Locale
+      .getDefault());
+
+  public static Splittable create(boolean value) {
+    return JsonSplittable.create(String.valueOf(value));
+  }
+
+  public static Splittable create(double value) {
+    return JsonSplittable.create(String.valueOf(value));
+  }
+
+  public static Splittable create(String value) {
+    return JsonSplittable.create(quote(value));
+  }
+
+  public static Splittable createIndexed() {
+    return JsonSplittable.createIndexed();
+  }
+
+  public static Splittable createSplittable() {
+    return JsonSplittable.create();
+  }
+
+  public static Splittable nullValue() {
+    return JsonSplittable.createNull();
+  }
 
   /**
    * Create a quoted JSON string.
diff --git a/user/src/com/google/gwt/core/client/JsonUtils.java b/user/src/com/google/gwt/core/client/JsonUtils.java
index e44bb35..7088266 100644
--- a/user/src/com/google/gwt/core/client/JsonUtils.java
+++ b/user/src/com/google/gwt/core/client/JsonUtils.java
@@ -60,17 +60,17 @@
       try {
         return JSON.parse(json);
       } catch (e) {
-        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json);
       }
     } else {
       if (!@com.google.gwt.core.client.JsonUtils::safeToEval(Ljava/lang/String;)(json)) {
-        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Illegal character in JSON string");
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Illegal character in JSON string", json);
       }
       json = @com.google.gwt.core.client.JsonUtils::escapeJsonForEval(Ljava/lang/String;)(json);
       try {
         return eval('(' + json + ')');
       } catch (e) {
-        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+        return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json);
       }
     }
   }-*/;
@@ -110,12 +110,12 @@
     try {
       return eval('(' + escaped + ')');
     } catch (e) {
-      return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e);
+      return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json);
     }
   }-*/;
 
-  static void throwIllegalArgumentException(String message) {
-    throw new IllegalArgumentException(message);
+  static void throwIllegalArgumentException(String message, String data) {
+    throw new IllegalArgumentException(message + "\n" + data);
   }
 
   private static native String escapeChar(String c) /*-{
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
index 6487320..429dd6f 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
@@ -26,8 +26,8 @@
 import com.google.gwt.autobean.shared.AutoBeanVisitor;
 import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.autobean.shared.ValueCodex;
+import com.google.gwt.autobean.shared.impl.AbstractAutoBean;
 import com.google.gwt.autobean.shared.impl.EnumMap;
-import com.google.gwt.autobean.shared.impl.LazySplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
 import com.google.gwt.event.shared.UmbrellaException;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
@@ -36,13 +36,13 @@
 import com.google.web.bindery.requestfactory.shared.EntityProxyId;
 import com.google.web.bindery.requestfactory.shared.Receiver;
 import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.RequestTransport.TransportReceiver;
 import com.google.web.bindery.requestfactory.shared.ServerFailure;
-import com.google.web.bindery.requestfactory.shared.ValueProxy;
 import com.google.web.bindery.requestfactory.shared.Violation;
 import com.google.web.bindery.requestfactory.shared.WriteOperation;
-import com.google.web.bindery.requestfactory.shared.RequestTransport.TransportReceiver;
 import com.google.web.bindery.requestfactory.shared.impl.posers.DatePoser;
 import com.google.web.bindery.requestfactory.shared.messages.IdMessage;
+import com.google.web.bindery.requestfactory.shared.messages.IdMessage.Strength;
 import com.google.web.bindery.requestfactory.shared.messages.InvocationMessage;
 import com.google.web.bindery.requestfactory.shared.messages.JsonRpcRequest;
 import com.google.web.bindery.requestfactory.shared.messages.MessageFactory;
@@ -51,7 +51,6 @@
 import com.google.web.bindery.requestfactory.shared.messages.ResponseMessage;
 import com.google.web.bindery.requestfactory.shared.messages.ServerFailureMessage;
 import com.google.web.bindery.requestfactory.shared.messages.ViolationMessage;
-import com.google.web.bindery.requestfactory.shared.messages.IdMessage.Strength;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -67,8 +66,7 @@
 /**
  * Base implementations for RequestContext services.
  */
-public abstract class AbstractRequestContext implements RequestContext,
-    EntityCodex.EntitySource {
+public abstract class AbstractRequestContext implements RequestContext, EntityCodex.EntitySource {
   /**
    * Allows the payload dialect to be injected into the AbstractRequestContext
    * without the caller needing to be concerned with how the implementation
@@ -110,8 +108,7 @@
        * DialectImpl interface and restored to to AbstractRequestContext.
        */
       if (!invocations.isEmpty()) {
-        throw new RuntimeException(
-            "Only one invocation per request, pending backend support");
+        throw new RuntimeException("Only one invocation per request, pending backend support");
       }
       invocations.add(request);
       for (Object arg : request.getRequestData().getOrderedParameters()) {
@@ -152,20 +149,23 @@
 
       if (!raw.isNull("error")) {
         Splittable error = raw.get("error");
-        ServerFailure failure = new ServerFailure(
-            error.get("message").asString(), error.get("code").asString(),
-            payload, true);
+        ServerFailure failure =
+            new ServerFailure(error.get("message").asString(), error.get("code").asString(),
+                payload, true);
         fail(receiver, failure);
         return;
       }
 
       Splittable result = raw.get("result");
       @SuppressWarnings("unchecked")
-      Class<BaseProxy> target = (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType();
+      Class<BaseProxy> target =
+          (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType();
 
       SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target);
       AutoBean<BaseProxy> bean = createProxy(target, id);
-      AutoBeanCodex.decodeInto(result, bean);
+      // XXX expose this as a proper API
+      ((AbstractAutoBean<?>) bean).setData(result);
+      // AutoBeanCodex.decodeInto(result, bean);
 
       if (callback != null) {
         callback.onSuccess(bean.as());
@@ -178,9 +178,8 @@
     Splittable encode(Object obj) {
       Splittable value;
       if (obj == null) {
-        return LazySplittable.NULL;
-      } else if (obj.getClass().isEnum()
-          && getAutoBeanFactory() instanceof EnumMap) {
+        return Splittable.NULL;
+      } else if (obj.getClass().isEnum() && getAutoBeanFactory() instanceof EnumMap) {
         value = ValueCodex.encode(((EnumMap) getAutoBeanFactory()).getToken((Enum<?>) obj));
       } else if (ValueCodex.canDecode(obj.getClass())) {
         value = ValueCodex.encode(obj);
@@ -233,13 +232,13 @@
     }
 
     public void processPayload(final Receiver<Void> receiver, String payload) {
-      ResponseMessage response = AutoBeanCodex.decode(
-          MessageFactoryHolder.FACTORY, ResponseMessage.class, payload).as();
+      ResponseMessage response =
+          AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ResponseMessage.class, payload).as();
       if (response.getGeneralFailure() != null) {
         ServerFailureMessage failure = response.getGeneralFailure();
-        ServerFailure fail = new ServerFailure(failure.getMessage(),
-            failure.getExceptionType(), failure.getStackTrace(),
-            failure.isFatal());
+        ServerFailure fail =
+            new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure
+                .getStackTrace(), failure.isFatal());
 
         fail(receiver, fail);
         return;
@@ -266,13 +265,12 @@
           if (response.getStatusCodes().get(i)) {
             invocations.get(i).onSuccess(response.getInvocationResults().get(i));
           } else {
-            ServerFailureMessage failure = AutoBeanCodex.decode(
-                MessageFactoryHolder.FACTORY, ServerFailureMessage.class,
-                response.getInvocationResults().get(i)).as();
+            ServerFailureMessage failure =
+                AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ServerFailureMessage.class,
+                    response.getInvocationResults().get(i)).as();
             invocations.get(i).onFail(
-                new ServerFailure(failure.getMessage(),
-                    failure.getExceptionType(), failure.getStackTrace(),
-                    failure.isFatal()));
+                new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure
+                    .getStackTrace(), failure.isFatal()));
           }
         } catch (Throwable t) {
           if (causes == null) {
@@ -368,12 +366,14 @@
    * Objects are placed into this map by being passed into {@link #edit} or as
    * an invocation argument.
    */
-  private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>();
+  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.
    */
-  private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies = new HashMap<SimpleProxyId<?>, AutoBean<?>>();
+  private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies =
+      new HashMap<SimpleProxyId<?>, AutoBean<?>>();
 
   /**
    * A map that allows us to handle the case where the server has sent back an
@@ -381,12 +381,12 @@
    * client will need to swap out the request-local ids with a regular
    * client-allocated id.
    */
-  private final Map<Integer, SimpleProxyId<?>> syntheticIds = new HashMap<Integer, SimpleProxyId<?>>();
+  private final Map<Integer, SimpleProxyId<?>> syntheticIds =
+      new HashMap<Integer, SimpleProxyId<?>>();
 
   private final DialectImpl dialect;
 
-  protected AbstractRequestContext(AbstractRequestFactory factory,
-      Dialect dialect) {
+  protected AbstractRequestContext(AbstractRequestFactory factory, Dialect dialect) {
     this.requestFactory = factory;
     this.dialect = dialect.create(this);
   }
@@ -405,12 +405,10 @@
   /**
    * Creates a new proxy with an assigned ID.
    */
-  public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz,
-      SimpleProxyId<T> id) {
+  public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id) {
     AutoBean<T> created = getAutoBeanFactory().create(clazz);
     if (created == null) {
-      throw new IllegalArgumentException("Unknown proxy type "
-          + clazz.getName());
+      throw new IllegalArgumentException("Unknown proxy type " + clazz.getName());
     }
     created.setTag(STABLE_ID, id);
     return created;
@@ -481,10 +479,9 @@
   /**
    * EntityCodex support.
    */
-  public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
-      Splittable serializedProxyId) {
-    IdMessage ref = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
-        IdMessage.class, serializedProxyId).as();
+  public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(Splittable serializedProxyId) {
+    IdMessage ref =
+        AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, IdMessage.class, serializedProxyId).as();
     @SuppressWarnings("unchecked")
     SimpleProxyId<Q> id = (SimpleProxyId<Q>) getId(ref);
     return getProxyForReturnPayloadGraph(id);
@@ -566,8 +563,7 @@
   protected void fail(Receiver<Void> receiver, ServerFailure failure) {
     reuse();
     Set<Throwable> causes = null;
-    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
-        invocations)) {
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) {
       try {
         request.onFail(failure);
       } catch (Throwable t) {
@@ -606,8 +602,7 @@
   protected void violation(final Receiver<Void> receiver, Set<Violation> errors) {
     reuse();
     Set<Throwable> causes = null;
-    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
-        invocations)) {
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) {
       try {
         request.onViolation(errors);
       } catch (Throwable t) {
@@ -640,16 +635,14 @@
     if (Strength.SYNTHETIC.equals(op.getStrength())) {
       return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
     }
-    return requestFactory.getId(op.getTypeToken(), op.getServerId(),
-        op.getClientId());
+    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) {
+  <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(SimpleProxyId<Q> id) {
     @SuppressWarnings("unchecked")
     AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id);
     if (bean == null) {
@@ -665,8 +658,8 @@
    * Create a single OperationMessage that encapsulates the state of a proxy
    * AutoBean.
    */
-  AutoBean<OperationMessage> makeOperationMessage(
-      SimpleProxyId<BaseProxy> stableId, AutoBean<?> proxyBean, boolean useDelta) {
+  AutoBean<OperationMessage> makeOperationMessage(SimpleProxyId<BaseProxy> stableId,
+      AutoBean<?> proxyBean, boolean useDelta) {
 
     // The OperationMessages describes operations on exactly one entity
     AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation();
@@ -710,8 +703,9 @@
     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);
+      diff =
+          useDelta ? AutoBeanUtils.diff(parent, proxyBean) : AutoBeanUtils
+              .getAllProperties(proxyBean);
     } else if (isValueType(stableId.getProxyClass())) {
       // Send everything
       diff = AutoBeanUtils.getAllProperties(proxyBean);
@@ -720,8 +714,7 @@
     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()));
+        propertyMap.put(entry.getKey(), EntityCodex.encode(this, entry.getValue()));
       }
       operation.setPropertyMap(propertyMap);
     }
@@ -735,8 +728,8 @@
    * @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) {
+  <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());
@@ -746,15 +739,16 @@
       // Apply updates
       toMutate.accept(new AutoBeanVisitor() {
         @Override
-        public boolean visitReferenceProperty(String propertyName,
-            AutoBean<?> value, PropertyContext ctx) {
+        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);
+              Class<?> elementType =
+                  ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx)
+                      .getElementType() : null;
+              Object decoded =
+                  EntityCodex.decode(AbstractRequestContext.this, ctx.getType(), elementType, raw);
               ctx.set(decoded);
             }
           }
@@ -762,8 +756,7 @@
         }
 
         @Override
-        public boolean visitValueProperty(String propertyName, Object value,
-            PropertyContext ctx) {
+        public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
           if (ctx.canSet()) {
             if (properties.containsKey(propertyName)) {
               Splittable raw = properties.get(propertyName);
@@ -799,8 +792,8 @@
           continue;
         }
         requestFactory.getEventBus().fireEventFromSource(
-            new EntityProxyChange<EntityProxy>((EntityProxy) proxy,
-                writeOperation), id.getProxyClass());
+            new EntityProxyChange<EntityProxy>((EntityProxy) proxy, writeOperation),
+            id.getProxyClass());
       }
     }
     return proxy;
@@ -811,8 +804,8 @@
    * 
    * @see #syntheticIds
    */
-  private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId(
-      String typeToken, int syntheticId) {
+  private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId(String typeToken,
+      int syntheticId) {
     @SuppressWarnings("unchecked")
     SimpleProxyId<Q> toReturn = (SimpleProxyId<Q>) syntheticIds.get(syntheticId);
     if (toReturn == null) {
@@ -861,19 +854,25 @@
    * Shallow-clones an autobean and makes duplicates of the collection types. A
    * regular {@link AutoBean#clone} won't duplicate reference properties.
    */
-  private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections(
-      AutoBean<T> toClone) {
-    AutoBean<T> clone = toClone.clone(false);
+  private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections(final AutoBean<T> toClone) {
+    AutoBean<T> clone = toClone.getFactory().create(toClone.getType());
+    clone.setTag(STABLE_ID, toClone.getTag(STABLE_ID));
+    clone.setTag(Constants.VERSION_PROPERTY_B64, toClone.getTag(Constants.VERSION_PROPERTY_B64));
     /*
      * Take ownership here to prevent cycles in value objects from overflowing
      * the stack.
      */
     takeOwnership(clone);
     clone.accept(new AutoBeanVisitor() {
+      final Map<String, Object> values = AutoBeanUtils.getAllProperties(toClone);
 
       @Override
-      public boolean visitCollectionProperty(String propertyName,
-          AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
+      public boolean visitCollectionProperty(String propertyName, AutoBean<Collection<?>> value,
+          CollectionPropertyContext ctx) {
+        // javac generics bug
+        value =
+            AutoBeanUtils.<Collection<?>, Collection<?>> getAutoBean((Collection<?>) values
+                .get(propertyName));
         if (value != null) {
           Collection<Object> collection;
           if (List.class == ctx.getType()) {
@@ -885,20 +884,20 @@
             throw new IllegalArgumentException(ctx.getType().getName());
           }
 
-          if (isValueType(ctx.getElementType())) {
+          if (isValueType(ctx.getElementType()) || isEntityType(ctx.getElementType())) {
             /*
-             * Value proxies must be cloned upfront, since the value is replaced
-             * outright.
+             * Proxies must be edited up-front so that the elements in the
+             * collection have stable identity.
              */
             for (Object o : value.as()) {
               if (o == null) {
                 collection.add(null);
               } else {
-                collection.add(editProxy((ValueProxy) o));
+                collection.add(editProxy((BaseProxy) o));
               }
             }
           } else {
-            // For entities and simple values, just alias the values
+            // For simple values, just copy the values
             collection.addAll(value.as());
           }
 
@@ -908,21 +907,30 @@
       }
 
       @Override
-      public boolean visitReferenceProperty(String propertyName,
-          AutoBean<?> value, PropertyContext ctx) {
+      public boolean visitReferenceProperty(String propertyName, AutoBean<?> value,
+          PropertyContext ctx) {
+        value = AutoBeanUtils.getAutoBean(values.get(propertyName));
         if (value != null) {
-          if (isValueType(ctx.getType())) {
+          if (isValueType(ctx.getType()) || isEntityType(ctx.getType())) {
             /*
              * Value proxies must be cloned upfront, since the value is replaced
              * outright.
              */
             @SuppressWarnings("unchecked")
-            AutoBean<ValueProxy> valueBean = (AutoBean<ValueProxy>) value;
+            AutoBean<BaseProxy> valueBean = (AutoBean<BaseProxy>) value;
             ctx.set(editProxy(valueBean.as()));
+          } else {
+            ctx.set(value.as());
           }
         }
         return false;
       }
+
+      @Override
+      public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) {
+        ctx.set(values.get(propertyName));
+        return false;
+      }
     });
     return clone;
   }
@@ -988,8 +996,7 @@
       }
 
       // Parameter values or references
-      List<Splittable> parameters = new ArrayList<Splittable>(
-          data.getOrderedParameters().length);
+      List<Splittable> parameters = new ArrayList<Splittable>(data.getOrderedParameters().length);
       for (Object param : data.getOrderedParameters()) {
         parameters.add(EntityCodex.encode(this, param));
       }
@@ -1008,8 +1015,8 @@
   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();
+      OperationMessage operation =
+          makeOperationMessage(BaseProxyCategory.stableId(currentView), currentView, true).as();
       operations.add(operation);
     }
     return operations;
@@ -1080,7 +1087,7 @@
    */
   private <T extends BaseProxy> T takeOwnership(AutoBean<T> bean) {
     editedProxies.put(stableId(bean), bean);
-    bean.setTag(REQUEST_CONTEXT, AbstractRequestContext.this);
+    bean.setTag(REQUEST_CONTEXT, this);
     return bean.as();
   }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java
index 5566589..8aca80f 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java
@@ -19,7 +19,6 @@
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.autobean.shared.ValueCodex;
-import com.google.gwt.autobean.shared.impl.LazySplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.EntityProxyId;
@@ -42,8 +41,7 @@
      * Expects an encoded
      * {@link com.google.web.bindery.requestfactory.shared.messages.IdMessage}.
      */
-    <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
-        Splittable serializedIdMessage);
+    <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(Splittable serializedIdMessage);
 
     /**
      * Should return an encoded
@@ -59,9 +57,9 @@
   /**
    * Collection support is limited to value types and resolving ids.
    */
-  public static Object decode(EntitySource source, Class<?> type,
-      Class<?> elementType, Splittable split) {
-    if (split == null || split == LazySplittable.NULL) {
+  public static Object decode(EntitySource source, Class<?> type, Class<?> elementType,
+      Splittable split) {
+    if (split == null || split == Splittable.NULL) {
       return null;
     }
 
@@ -99,8 +97,7 @@
       return collection;
     }
 
-    if (source.isEntityType(type) || source.isValueType(type)
-        || EntityProxyId.class.equals(type)) {
+    if (source.isEntityType(type) || source.isValueType(type) || EntityProxyId.class.equals(type)) {
       return source.getBeanForPayload(split).as();
     }
 
@@ -111,8 +108,8 @@
   /**
    * Collection support is limited to value types and resolving ids.
    */
-  public static Object decode(EntitySource source, Class<?> type,
-      Class<?> elementType, String jsonPayload) {
+  public static Object decode(EntitySource source, Class<?> type, Class<?> elementType,
+      String jsonPayload) {
     Splittable split = StringQuoter.split(jsonPayload);
     return decode(source, type, elementType, split);
   }
@@ -122,7 +119,7 @@
    */
   public static Splittable encode(EntitySource source, Object value) {
     if (value == null) {
-      return LazySplittable.NULL;
+      return Splittable.NULL;
     }
 
     if (value instanceof Poser<?>) {
@@ -146,7 +143,7 @@
         }
       }
       toReturn.append(']');
-      return new LazySplittable(toReturn.toString());
+      return StringQuoter.split(toReturn.toString());
     }
 
     if (value instanceof BaseProxy) {
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 cb331b6..bfd0300 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
@@ -17,6 +17,8 @@
 
 import com.google.gwt.autobean.client.impl.JsoSplittable;
 import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.GwtScriptOnly;
 import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.core.client.JsDate;
 import com.google.gwt.core.client.JsonUtils;
@@ -27,18 +29,44 @@
 /**
  * This a super-source version with a client-only implementation.
  */
+@GwtScriptOnly
 public class StringQuoter {
+  public static Splittable create(boolean value) {
+    return JsoSplittable.create(value);
+  }
+
+  public static Splittable create(double value) {
+    return JsoSplittable.create(value);
+  }
+
+  public static Splittable create(String value) {
+    return JsoSplittable.create(value);
+  }
+
+  public static Splittable createIndexed() {
+    return JsoSplittable.createIndexed();
+  }
+
+  public static Splittable createSplittable() {
+    return JsoSplittable.create();
+  }
+
+  public static Splittable nullValue() {
+    return JsoSplittable.nullValue();
+  }
+
   public static String quote(String raw) {
     return JsonUtils.escapeValue(raw);
   }
 
   public static Splittable split(String payload) {
-    boolean isString = payload.charAt(0) == '\"';
-    if (isString) {
+    char c = payload.charAt(0);
+    boolean isSimple = c != '{' && c != '[';
+    if (isSimple) {
       payload = "[" + payload + "]";
     }
-    Splittable toReturn = JsoSplittable.create(JsonUtils.safeEval(payload));
-    if (isString) {
+    Splittable toReturn = JsonUtils.safeEval(payload).<JsoSplittable> cast();
+    if (isSimple) {
       toReturn = toReturn.get(0);
     }
     return toReturn;
diff --git a/user/test/com/google/gwt/autobean/AutoBeanSuite.java b/user/test/com/google/gwt/autobean/AutoBeanSuite.java
index 491e91c..f8f8d6e 100644
--- a/user/test/com/google/gwt/autobean/AutoBeanSuite.java
+++ b/user/test/com/google/gwt/autobean/AutoBeanSuite.java
@@ -18,7 +18,9 @@
 import com.google.gwt.autobean.client.AutoBeanTest;
 import com.google.gwt.autobean.server.AutoBeanCodexJreTest;
 import com.google.gwt.autobean.server.AutoBeanJreTest;
+import com.google.gwt.autobean.server.SplittableJreTest;
 import com.google.gwt.autobean.shared.AutoBeanCodexTest;
+import com.google.gwt.autobean.shared.SplittableTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
 import junit.framework.Test;
@@ -34,6 +36,8 @@
     suite.addTestSuite(AutoBeanCodexTest.class);
     suite.addTestSuite(AutoBeanJreTest.class);
     suite.addTestSuite(AutoBeanTest.class);
+    suite.addTestSuite(SplittableJreTest.class);
+    suite.addTestSuite(SplittableTest.class);
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
index cc61684..e83e46d 100644
--- a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
+++ b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
@@ -263,46 +263,6 @@
     assertEquals("Blah", more.as().getString());
   }
 
-  public void testClone() {
-    AutoBean<Intf> a1 = factory.intf();
-
-    Intf i1 = a1.as();
-    i1.setInt(42);
-
-    AutoBean<Intf> a2 = a1.clone(false);
-    Intf i2 = a2.as();
-
-    // Copies have the same values
-    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);
-    assertEquals(41, i1.getInt());
-    assertEquals(42, i2.getInt());
-  }
-
-  public void testCloneDeep() {
-    AutoBean<OtherIntf> a1 = factory.otherIntf();
-    OtherIntf o1 = a1.as();
-
-    o1.setIntf(factory.intf().as());
-    o1.getIntf().setInt(42);
-
-    AutoBean<OtherIntf> a2 = a1.clone(true);
-    assertTrue(AutoBeanUtils.deepEquals(a1, a2));
-
-    OtherIntf o2 = a2.as();
-
-    assertNotSame(o1.getIntf(), o2.getIntf());
-    assertEquals(o1.getIntf().getInt(), o2.getIntf().getInt());
-
-    o1.getIntf().setInt(41);
-    assertEquals(42, o2.getIntf().getInt());
-  }
-
   public void testDiff() {
     AutoBean<Intf> a1 = factory.intf();
     AutoBean<Intf> a2 = factory.intf();
@@ -315,24 +275,6 @@
     assertEquals(42, diff.get("int"));
   }
 
-  /**
-   * Tests that lists are in fact aliased between AutoBeans after a shallow
-   * clone.
-   */
-  public void testDiffWithCloneAndListMutation() {
-    AutoBean<HasList> a1 = factory.hasList();
-    List<Intf> list = new ArrayList<Intf>();
-    list.add(factory.intf().as());
-    a1.as().setList(list);
-
-    AutoBean<HasList> a2 = a1.clone(false);
-    assertTrue(AutoBeanUtils.diff(a1, a2).isEmpty());
-
-    // Lists are aliased between AutoBeans
-    assertSame(a1.as().getList(), a2.as().getList());
-    assertEquals(a1.as().getList(), a2.as().getList());
-  }
-
   public void testDiffWithListPropertyAssignment() {
     AutoBean<HasList> a1 = factory.hasList();
     AutoBean<HasList> a2 = factory.hasList();
@@ -435,14 +377,14 @@
       }
 
       @Override
-      public void endVisitCollectionProperty(String propertyName,
-          AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
+      public void endVisitCollectionProperty(String propertyName, AutoBean<Collection<?>> value,
+          CollectionPropertyContext ctx) {
         check(propertyName, ctx);
       }
 
       @Override
-      public void endVisitMapProperty(String propertyName,
-          AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
+      public void endVisitMapProperty(String propertyName, AutoBean<Map<?, ?>> value,
+          MapPropertyContext ctx) {
         check(propertyName, ctx);
       }
 
@@ -458,14 +400,12 @@
         } else if ("listOfMap".equals(propertyName)) {
           // List<Map<String, Intf>>
           assertEquals(List.class.getName() + "<" + Map.class.getName() + "<"
-              + String.class.getName() + "," + Intf.class.getName() + ">>",
-              sb.toString());
+              + String.class.getName() + "," + Intf.class.getName() + ">>", sb.toString());
         } else if ("map".equals(propertyName)) {
           // Map<Map<String, String>, List<List<Intf>>>
           assertEquals(Map.class.getName() + "<" + Map.class.getName() + "<"
-              + String.class.getName() + "," + String.class.getName() + ">,"
-              + List.class.getName() + "<" + List.class.getName() + "<"
-              + Intf.class.getName() + ">>>", sb.toString());
+              + String.class.getName() + "," + String.class.getName() + ">," + List.class.getName()
+              + "<" + List.class.getName() + "<" + Intf.class.getName() + ">>>", sb.toString());
         } else {
           throw new RuntimeException(propertyName);
         }
@@ -512,8 +452,8 @@
       boolean seenOther;
 
       @Override
-      public void endVisitReferenceProperty(String propertyName,
-          AutoBean<?> value, PropertyContext ctx) {
+      public void endVisitReferenceProperty(String propertyName, AutoBean<?> value,
+          PropertyContext ctx) {
         if ("hasBoolean".equals(propertyName)) {
           assertSame(hasBoolean, value);
           assertEquals(HasBoolean.class, ctx.getType());
@@ -529,8 +469,7 @@
       }
 
       @Override
-      public void endVisitValueProperty(String propertyName, Object value,
-          PropertyContext ctx) {
+      public void endVisitValueProperty(String propertyName, Object value, PropertyContext ctx) {
         if ("int".equals(propertyName)) {
           assertEquals(42, value);
           assertEquals(int.class, ctx.getType());
diff --git a/user/test/com/google/gwt/autobean/server/SplittableJreTest.java b/user/test/com/google/gwt/autobean/server/SplittableJreTest.java
new file mode 100644
index 0000000..af1c93a
--- /dev/null
+++ b/user/test/com/google/gwt/autobean/server/SplittableJreTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 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.autobean.server;
+
+import com.google.gwt.autobean.shared.SplittableTest;
+
+/**
+ * A JRE-only version of SplittableTest.
+ */
+public class SplittableJreTest extends SplittableTest {
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+}
diff --git a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
index 8e686b8..43bfb35 100644
--- a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
+++ b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
@@ -200,7 +200,7 @@
     assertEquals(MyEnum.BAR, map.getEnum(MyEnum.class, "BAR"));
     assertEquals(MyEnum.BAZ, map.getEnum(MyEnum.class, "quux"));
 
-    List<MyEnum> arrayValue = Arrays.asList(MyEnum.FOO, MyEnum.BAR, MyEnum.BAZ);
+    List<MyEnum> arrayValue = Arrays.asList(MyEnum.FOO, MyEnum.BAR, null, MyEnum.BAZ);
     Map<MyEnum, Integer> mapValue = new HashMap<MyEnum, Integer>();
     mapValue.put(MyEnum.FOO, 0);
     mapValue.put(MyEnum.BAR, 1);
@@ -219,6 +219,13 @@
     assertEquals(MyEnum.BAZ, decoded.as().getEnum());
     assertEquals(arrayValue, decoded.as().getEnums());
     assertEquals(mapValue, decoded.as().getMap());
+
+    assertEquals(MyEnum.BAZ, AutoBeanUtils.getAllProperties(bean).get("enum"));
+    bean.as().setEnum(null);
+    assertNull(bean.as().getEnum());
+    assertNull(AutoBeanUtils.getAllProperties(bean).get("enum"));
+    decoded = checkEncode(bean);
+    assertNull(decoded.as().getEnum());
   }
 
   /**
@@ -229,14 +236,13 @@
     EnumMap map = (EnumMap) f;
     assertEquals("FOO_LIST", map.getToken(EnumReachableThroughList.FOO_LIST));
     assertEquals("FOO_KEY", map.getToken(EnumReachableThroughMapKey.FOO_KEY));
-    assertEquals("FOO_VALUE",
-        map.getToken(EnumReachableThroughMapValue.FOO_VALUE));
-    assertEquals(EnumReachableThroughList.FOO_LIST,
-        map.getEnum(EnumReachableThroughList.class, "FOO_LIST"));
-    assertEquals(EnumReachableThroughMapKey.FOO_KEY,
-        map.getEnum(EnumReachableThroughMapKey.class, "FOO_KEY"));
-    assertEquals(EnumReachableThroughMapValue.FOO_VALUE,
-        map.getEnum(EnumReachableThroughMapValue.class, "FOO_VALUE"));
+    assertEquals("FOO_VALUE", map.getToken(EnumReachableThroughMapValue.FOO_VALUE));
+    assertEquals(EnumReachableThroughList.FOO_LIST, map.getEnum(EnumReachableThroughList.class,
+        "FOO_LIST"));
+    assertEquals(EnumReachableThroughMapKey.FOO_KEY, map.getEnum(EnumReachableThroughMapKey.class,
+        "FOO_KEY"));
+    assertEquals(EnumReachableThroughMapValue.FOO_VALUE, map.getEnum(
+        EnumReachableThroughMapValue.class, "FOO_VALUE"));
   }
 
   public void testMap() {
@@ -267,8 +273,7 @@
     }
     assertEquals(5, complex.size());
     for (Map.Entry<Simple, Simple> entry : complex.entrySet()) {
-      assertEquals(entry.getKey().getString(),
-          String.valueOf(entry.getValue().getInt()));
+      assertEquals(entry.getKey().getString(), String.valueOf(entry.getValue().getInt()));
     }
   }
 
@@ -282,7 +287,8 @@
     Map<String, String> value = new HashMap<String, String>();
     value.put("c", "d");
 
-    Map<Map<String, String>, Map<String, String>> test = new HashMap<Map<String, String>, Map<String, String>>();
+    Map<Map<String, String>, Map<String, String>> test =
+        new HashMap<Map<String, String>, Map<String, String>>();
     test.put(key, value);
 
     AutoBean<HasMap> bean = f.hasMap();
@@ -316,8 +322,8 @@
 
     AutoBean<HasSimple> decodedBean2 = checkEncode(bean2);
     assertNotNull(decodedBean2.as().getSimple());
-    assertTrue(AutoBeanUtils.diff(bean,
-        AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple())).isEmpty());
+    assertTrue(AutoBeanUtils.diff(bean, AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple()))
+        .isEmpty());
 
     AutoBean<HasList> bean3 = f.hasList();
     bean3.as().setIntList(Arrays.asList(1, 2, 3, null, 4, 5));
@@ -325,8 +331,7 @@
 
     AutoBean<HasList> decodedBean3 = checkEncode(bean3);
     assertNotNull(decodedBean3.as().getIntList());
-    assertEquals(Arrays.asList(1, 2, 3, null, 4, 5),
-        decodedBean3.as().getIntList());
+    assertEquals(Arrays.asList(1, 2, 3, null, 4, 5), decodedBean3.as().getIntList());
     assertNotNull(decodedBean3.as().getList());
     assertEquals(1, decodedBean3.as().getList().size());
     assertTrue(AutoBeanUtils.diff(bean,
@@ -339,30 +344,25 @@
     AutoBean<HasSplittable> bean = f.hasAutoBean();
     bean.as().setSimple(AutoBeanCodex.encode(simple));
     bean.as().setString(ValueCodex.encode("Hello ['\"] world"));
-    List<Splittable> testList = Arrays.asList(AutoBeanCodex.encode(simple),
-        null, AutoBeanCodex.encode(simple));
+    List<Splittable> testList =
+        Arrays.asList(AutoBeanCodex.encode(simple), null, AutoBeanCodex.encode(simple));
     bean.as().setSimpleList(testList);
-    Map<Splittable, Splittable> testMap = Collections.singletonMap(
-        ValueCodex.encode("12345"), ValueCodex.encode("5678"));
+    Map<Splittable, Splittable> testMap =
+        Collections.singletonMap(ValueCodex.encode("12345"), ValueCodex.encode("5678"));
     bean.as().setSplittableMap(testMap);
 
     AutoBean<HasSplittable> decoded = checkEncode(bean);
     Splittable toDecode = decoded.as().getSimple();
-    AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class,
-        toDecode);
+    AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class, toDecode);
     assertEquals("Simple", decodedSimple.as().getString());
-    assertEquals("Hello ['\"] world",
-        ValueCodex.decode(String.class, decoded.as().getString()));
-    assertEquals("12345",
-        decoded.as().getSplittableMap().keySet().iterator().next().asString());
-    assertEquals("5678",
-        decoded.as().getSplittableMap().values().iterator().next().asString());
+    assertEquals("Hello ['\"] world", ValueCodex.decode(String.class, decoded.as().getString()));
+    assertEquals("12345", decoded.as().getSplittableMap().keySet().iterator().next().asString());
+    assertEquals("5678", decoded.as().getSplittableMap().values().iterator().next().asString());
 
     List<Splittable> list = decoded.as().getSimpleList();
     assertEquals(3, list.size());
     assertNull(list.get(1));
-    assertEquals("Simple",
-        AutoBeanCodex.decode(f, Simple.class, list.get(2)).as().getString());
+    assertEquals("Simple", AutoBeanCodex.decode(f, Simple.class, list.get(2)).as().getString());
   }
 
   @Override
diff --git a/user/test/com/google/gwt/autobean/shared/SplittableTest.java b/user/test/com/google/gwt/autobean/shared/SplittableTest.java
new file mode 100644
index 0000000..46c515b
--- /dev/null
+++ b/user/test/com/google/gwt/autobean/shared/SplittableTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2011 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.autobean.shared;
+
+import com.google.gwt.autobean.client.impl.JsoSplittable;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder;
+import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState;
+import com.google.gwt.autobean.shared.impl.SplittableList;
+import com.google.gwt.autobean.shared.impl.SplittableSimpleMap;
+import com.google.gwt.autobean.shared.impl.StringQuoter;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for the underlying Splittable implementation. This test class is not
+ * indicative of code that users would write, it's simply doing spot-checks of
+ * functionality that AbstractAutoBean depends on.
+ */
+public class SplittableTest extends GWTTestCase {
+
+  /**
+   * 
+   */
+  private static final EncodeState testState = EncodeState.forTesting();
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.autobean.AutoBean";
+  }
+
+  public void testBasicProperties() {
+    Splittable data = StringQuoter.split("{\"a\":true, \"b\":3, \"c\":\"string\", \"d\":null}");
+    assertTrue("isBoolean", data.get("a").isBoolean());
+    assertTrue("asBoolean", data.get("a").asBoolean());
+    assertTrue("isNumber", data.get("b").isNumber());
+    assertEquals(3.0, data.get("b").asNumber());
+    assertTrue("isString", data.get("c").isString());
+    assertEquals("string", data.get("c").asString());
+    assertTrue("isNull", data.isNull("d"));
+    assertNull("should be null", data.get("d"));
+  }
+
+  /**
+   * Ensure that hashcodes don't leak into the payload.
+   */
+  public void testHashCode() {
+    Splittable data = StringQuoter.split("{\"a\":\"b\"}");
+    int hash = data.hashCode();
+    String payload = data.getPayload();
+    assertFalse(payload, payload.contains("$H"));
+    assertFalse(payload, payload.contains(String.valueOf(hash)));
+    assertEquals(hash, data.hashCode());
+  }
+
+  /**
+   * Splittables are implemented by a couple of different concrete types. We'll
+   * use this method to make sure that the correct implementation type is being
+   * used in various circumstances.
+   */
+  public void testImplementationChoice() {
+    Splittable s = StringQuoter.split("[1,false,\"true\"]");
+    if (GWT.isScript()) {
+      assertTrue("s should be JsoSplittable", s instanceof JsoSplittable);
+      assertTrue("s[0] should be JsoSplittable", s.get(0) instanceof JsoSplittable);
+      assertTrue("s[1] should be JsoSplittable", s.get(1) instanceof JsoSplittable);
+      assertTrue("s[2] should be JsoSplittable", s.get(2) instanceof JsoSplittable);
+    } else {
+      // Using the same types in both pure-JRE and DevMode to avoid JSNI
+      // overhead
+      assertTrue("s should be JsonSplittable", s.getClass().getName().endsWith("JsonSplittable"));
+      assertTrue("s[0] should be JsonSplittable", s.get(0).getClass().getName().endsWith(
+          "JsonSplittable"));
+      assertTrue("s[1] should be JsonSplittable", s.get(1).getClass().getName().endsWith(
+          "JsonSplittable"));
+      assertTrue("s[2] should be JsonSplittable", s.get(2).getClass().getName().endsWith(
+          "JsonSplittable"));
+    }
+  }
+
+  public void testIndexed() {
+    Splittable s = StringQuoter.createIndexed();
+    assertTrue(s.isIndexed());
+    assertFalse(s.isKeyed());
+    assertFalse(s.isString());
+    assertEquals(0, s.size());
+
+    string("foo").assign(s, 0);
+    string("bar").assign(s, 1);
+    string("baz").assign(s, 2);
+
+    assertEquals(3, s.size());
+    assertEquals("[\"foo\",\"bar\",\"baz\"]", s.getPayload());
+
+    string("quux").assign(s, 1);
+    assertEquals("[\"foo\",\"quux\",\"baz\"]", s.getPayload());
+
+    Splittable s2 = s.deepCopy();
+    assertNotSame(s, s2);
+    assertEquals(s.size(), s2.size());
+    for (int i = 0, j = s.size(); i < j; i++) {
+      assertEquals(s.get(i).asString(), s2.get(i).asString());
+    }
+
+    s.setSize(2);
+    assertEquals(2, s.size());
+    assertEquals("[\"foo\",\"quux\"]", s.getPayload());
+
+    // Make sure reified values aren't in the payload
+    Object o = new Object();
+    s.setReified("reified", o);
+    assertFalse(s.getPayload().contains("reified"));
+    assertFalse(s.getPayload().contains("__s"));
+    assertSame(o, s.getReified("reified"));
+  }
+
+  public void testKeyed() {
+    Splittable s = StringQuoter.createSplittable();
+    assertFalse(s.isIndexed());
+    assertTrue(s.isKeyed());
+    assertFalse(s.isString());
+    assertTrue(s.getPropertyKeys().isEmpty());
+
+    string("bar").assign(s, "foo");
+    string("quux").assign(s, "baz");
+
+    // Actual iteration order is undefined
+    assertEquals(new HashSet<String>(Arrays.asList("foo", "baz")), new HashSet<String>(s
+        .getPropertyKeys()));
+
+    assertFalse(s.isNull("foo"));
+    assertTrue(s.isNull("bar"));
+
+    assertEquals("bar", s.get("foo").asString());
+    assertEquals("quux", s.get("baz").asString());
+
+    String payload = s.getPayload();
+    assertTrue(payload.startsWith("{"));
+    assertTrue(payload.endsWith("}"));
+    assertTrue(payload.contains("\"foo\":\"bar\""));
+    assertTrue(payload.contains("\"baz\":\"quux\""));
+
+    Splittable s2 = s.deepCopy();
+    assertNotSame(s, s2);
+    assertEquals("bar", s2.get("foo").asString());
+    assertEquals("quux", s2.get("baz").asString());
+
+    // Make sure reified values aren't in the payload
+    Object o = new Object();
+    s.setReified("reified", o);
+    assertFalse("Should not see reified in " + s.getPayload(), s.getPayload().contains("reified"));
+    assertFalse("Should not see __s in " + s.getPayload(), s.getPayload().contains("__s"));
+    assertSame(o, s.getReified("reified"));
+  }
+
+  public void testNested() {
+    Splittable s = StringQuoter.split("{\"a\":{\"foo\":\"bar\"}}");
+    Splittable a = s.get("a");
+    assertNotNull(a);
+    assertEquals("bar", a.get("foo").asString());
+    assertSame(a, s.get("a"));
+    assertEquals(a, s.get("a"));
+  }
+
+  /**
+   * Tests attributes of the {@link Splittable#NULL} field.
+   */
+  public void testNull() {
+    Splittable n = Splittable.NULL;
+    if (GWT.isScript()) {
+      assertNull(n);
+    } else {
+      assertNotNull(n);
+    }
+    assertFalse("boolean", n.isBoolean());
+    assertFalse("indexed", n.isIndexed());
+    assertFalse("keyed", n.isKeyed());
+    assertFalse("string", n.isString());
+    assertEquals("null", n.getPayload());
+  }
+
+  /**
+   * Extra tests in here due to potential to confuse {@code false} and
+   * {@code null} values.
+   */
+  public void testSplittableListBoolean() {
+    Coder boolCoder = AutoBeanCodexImpl.valueCoder(Boolean.class);
+    Splittable s = StringQuoter.createIndexed();
+    bool(false).assign(s, 0);
+    assertFalse("0 should not be null", s.isNull(0));
+    assertTrue("s[0] should be a boolean", s.get(0).isBoolean());
+    assertFalse("s[0] should be false", s.get(0).asBoolean());
+    assertNotNull("Null decode", ValueCodex.decode(Boolean.class, s.get(0)));
+    Object decodedBoolean = boolCoder.decode(testState, s.get(0));
+    assertNotNull("decode should not return null", decodedBoolean);
+    assertFalse("decoded value should be false", (Boolean) decodedBoolean);
+
+    bool(true).assign(s, 1);
+    assertTrue("s[1] should be a boolean", s.get(1).isBoolean());
+    assertTrue("s[1] should be true", s.get(1).asBoolean());
+    assertTrue("boolCoder 1", (Boolean) boolCoder.decode(testState, s.get(1)));
+
+    Splittable.NULL.assign(s, 2);
+    assertTrue("3 should be null", s.isNull(3));
+    assertEquals("payload", "[false,true,null]", s.getPayload());
+    List<Boolean> boolList = new SplittableList<Boolean>(s, boolCoder, testState);
+    assertEquals("boolList", Arrays.<Boolean> asList(false, true, null), boolList);
+  }
+
+  /**
+   * Extra tests in here due to potential to confuse 0 and {@code null} values.
+   */
+  public void testSplittableListNumbers() {
+    Coder intCoder = AutoBeanCodexImpl.valueCoder(Integer.class);
+    Coder doubleCoder = AutoBeanCodexImpl.valueCoder(Double.class);
+    Splittable s = StringQuoter.createIndexed();
+    number(0).assign(s, 0);
+    assertFalse("0 should not be null", s.isNull(0));
+    assertTrue("s[0] should be a number", s.get(0).isNumber());
+    assertNotNull("Null decode", ValueCodex.decode(Integer.class, s.get(0)));
+    Object decodedInt = intCoder.decode(testState, s.get(0));
+    assertNotNull("decode should not return null", decodedInt);
+    assertEquals("intCoder 0", Integer.valueOf(0), decodedInt);
+    assertEquals("doubleCoder 0", Double.valueOf(0), doubleCoder.decode(testState, s.get(0)));
+
+    number(3.141592).assign(s, 1);
+    assertEquals("intCoder 1", Integer.valueOf(3), intCoder.decode(testState, s.get(1)));
+    assertEquals("doubleCoder 1", Double.valueOf(3.141592), doubleCoder.decode(testState, s.get(1)));
+
+    number(42).assign(s, 2);
+    Splittable.NULL.assign(s, 3);
+    assertTrue("3 should be null", s.isNull(3));
+    assertEquals("payload", "[0,3.141592,42,null]", s.getPayload());
+    List<Double> doubleList = new SplittableList<Double>(s, doubleCoder, testState);
+    assertEquals(Double.valueOf(0), doubleList.get(0));
+    assertEquals("doubleList", Arrays.<Double> asList(0d, 3.141592, 42d, null), doubleList);
+
+    // Don't share backing data between lists
+    s = StringQuoter.split("[0,3.141592,42,null]");
+    List<Integer> intList = new SplittableList<Integer>(s, intCoder, testState);
+    assertEquals("intList", Arrays.<Integer> asList(0, 3, 42, null), intList);
+  }
+
+  public void testSplittableListString() {
+    Splittable data = StringQuoter.split("[\"Hello\",\"World\"]");
+    SplittableList<String> list =
+        new SplittableList<String>(data, AutoBeanCodexImpl.valueCoder(String.class), testState);
+    assertEquals(2, list.size());
+    assertEquals(Arrays.asList("Hello", "World"), list);
+    list.set(0, "Goodbye");
+    assertEquals(Arrays.asList("Goodbye", "World"), list);
+    assertEquals("[\"Goodbye\",\"World\"]", data.getPayload());
+    list.remove(0);
+    assertEquals(Arrays.asList("World"), list);
+    assertEquals("[\"World\"]", data.getPayload());
+    list.add("Wide");
+    list.add("Web");
+    assertEquals(Arrays.asList("World", "Wide", "Web"), list);
+    assertEquals("[\"World\",\"Wide\",\"Web\"]", data.getPayload());
+  }
+
+  public void testSplittableMapStringString() {
+    Splittable data = StringQuoter.split("{\"foo\":\"bar\",\"baz\":\"quux\",\"isNull\":null}");
+    assertTrue("isNull should be null", data.isNull("isNull"));
+    assertFalse("isNull should not be undefined", data.isUndefined("isNull"));
+    Map<String, String> map =
+        new SplittableSimpleMap<String, String>(data, AutoBeanCodexImpl.valueCoder(String.class),
+            AutoBeanCodexImpl.valueCoder(String.class), testState);
+    assertEquals(3, map.size());
+    assertEquals("bar", map.get("foo"));
+    assertEquals("quux", map.get("baz"));
+    assertTrue("Map should have isNull key", map.containsKey("isNull"));
+    assertNull(map.get("isNull"));
+    assertFalse("Map should not have unknown key", map.containsKey("unknown"));
+
+    map.put("bar", "foo2");
+    assertEquals("foo2", map.get("bar"));
+    String payload = data.getPayload();
+    assertTrue(payload.contains("\"bar\":\"foo2\""));
+    assertTrue(payload.contains("\"isNull\":null"));
+  }
+
+  public void testString() {
+    Splittable s = string("Hello '\" World!");
+    assertFalse(s.isIndexed());
+    assertFalse(s.isKeyed());
+    assertTrue(s.isString());
+    assertEquals("Hello '\" World!", s.asString());
+    assertEquals("\"Hello '\\\" World!\"", s.getPayload());
+  }
+
+  public void testStringEmpty() {
+    Splittable s = string("");
+    assertFalse(s.isIndexed());
+    assertFalse(s.isKeyed());
+    assertTrue(s.isString());
+    assertEquals("", s.asString());
+    assertEquals("\"\"", s.getPayload());
+  }
+
+  private Splittable bool(boolean value) {
+    return StringQuoter.split(String.valueOf(value));
+  }
+
+  private Splittable number(double number) {
+    return StringQuoter.split(String.valueOf(number));
+  }
+
+  private Splittable string(String value) {
+    return StringQuoter.split(StringQuoter.quote(value));
+  }
+}