Initial add of JSON-RPC support to RequestFactory.
Patch by: bobv
Review by: rjrjr
Review at http://gwt-code-reviews.appspot.com/1355804


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9741 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBean.java b/user/src/com/google/gwt/autobean/shared/AutoBean.java
index a51c22b..8b8613f 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBean.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBean.java
@@ -37,7 +37,7 @@
    */
   @Documented
   @Retention(RetentionPolicy.RUNTIME)
-  @Target(value = {ElementType.METHOD, ElementType.FIELD})
+  @Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
   public @interface PropertyName {
     String value();
   }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
index ec564d6..bcded2c 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -423,6 +423,17 @@
   }
 
   /**
+   * Copy data from a {@link Splittable} into an AutoBean. Unset values in the
+   * Splittable will not nullify data that already exists in the AutoBean.
+   * 
+   * @param data the source data to copy
+   * @param bean the target AutoBean
+   */
+  public static void decodeInto(Splittable data, AutoBean<?> bean) {
+    new AutoBeanCodex(bean.getFactory()).doDecodeInto(data, bean);
+  }
+
+  /**
    * Encodes an AutoBean. The actual payload contents can be retrieved through
    * {@link Splittable#getPayload()}.
    * 
diff --git a/user/src/com/google/gwt/autobean/shared/ValueCodex.java b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
index ca867eb..b4fb321 100644
--- a/user/src/com/google/gwt/autobean/shared/ValueCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
@@ -89,7 +89,7 @@
 
       @Override
       public Date decode(Class<?> clazz, String value) {
-        return new Date(Long.valueOf(value));
+        return StringQuoter.tryParseDate(value);
       }
 
       @Override
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 15ce275..899ed52 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -20,10 +20,24 @@
 
 import org.json.JSONObject;
 
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
 /**
  * This class has a super-source version with a client-only implementation.
  */
 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 String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z";
+  private static final DateFormat RFC2822 = new SimpleDateFormat(
+      RFC2822_PATTERN, Locale.getDefault());
+
   /**
    * Create a quoted JSON string.
    */
@@ -34,4 +48,27 @@
   public static Splittable split(String payload) {
     return JsonSplittable.create(payload);
   }
+
+  /**
+   * Attempt to parse an ISO-8601 date format. May return {@code null} if the
+   * input cannot be parsed.
+   */
+  public static Date tryParseDate(String date) {
+    try {
+      return new Date(Long.parseLong(date));
+    } catch (NumberFormatException ignored) {
+    }
+    if (date.endsWith("Z")) {
+      date = date.substring(0, date.length() - 1) + "+0000";
+    }
+    try {
+      return ISO8601.parse(date);
+    } catch (ParseException ignored) {
+    }
+    try {
+      return RFC2822.parse(date);
+    } catch (ParseException ignored) {
+    }
+    return null;
+  }
 }
diff --git a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
index 89c992a..d8ff630 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
@@ -15,7 +15,9 @@
  */
 package com.google.gwt.requestfactory.rebind;
 
+import com.google.gwt.autobean.rebind.model.JBeanMethod;
 import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
 import com.google.gwt.autobean.shared.AutoBeanFactory;
 import com.google.gwt.autobean.shared.AutoBeanFactory.Category;
 import com.google.gwt.autobean.shared.AutoBeanFactory.NoWrap;
@@ -37,8 +39,10 @@
 import com.google.gwt.requestfactory.rebind.model.RequestFactoryModel;
 import com.google.gwt.requestfactory.rebind.model.RequestMethod;
 import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.JsonRpcContent;
 import com.google.gwt.requestfactory.shared.impl.AbstractRequest;
 import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
 import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
 import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
 import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory;
@@ -145,15 +149,16 @@
       SourceWriter sw = factory.createSourceWriter(context, pw);
 
       // Constructor that accepts the parent RequestFactory
-      sw.println("public %s(%s requestFactory) {super(requestFactory);}",
+      sw.println(
+          "public %s(%s requestFactory) {super(requestFactory, %s.%s);}",
           method.getSimpleSourceName(),
-          AbstractRequestFactory.class.getCanonicalName());
+          AbstractRequestFactory.class.getCanonicalName(),
+          Dialect.class.getCanonicalName(), method.getDialect().name());
 
       // Write each Request method
       for (RequestMethod request : method.getRequestMethods()) {
         JMethod jmethod = request.getDeclarationMethod();
-        String operation = jmethod.getEnclosingType().getQualifiedBinaryName()
-            + "::" + jmethod.getName();
+        String operation = request.getOperation();
 
         // foo, bar, baz
         StringBuilder parameterArray = new StringBuilder();
@@ -223,12 +228,54 @@
             returnTypeBaseQualifiedName, elementType);
         sw.println("}");
 
+        /*
+         * Only support extra properties in JSON-RPC payloads. Could add this to
+         * standard requests to provide out-of-band data.
+         */
+        if (method.getDialect().equals(Dialect.JSON_RPC)) {
+          for (JMethod setter : request.getExtraSetters()) {
+            PropertyName propertyNameAnnotation = setter.getAnnotation(PropertyName.class);
+            String propertyName = propertyNameAnnotation == null
+                ? JBeanMethod.SET.inferName(setter)
+                : propertyNameAnnotation.value();
+            String maybeReturn = JBeanMethod.SET_BUILDER.matches(setter)
+                ? "return this;" : "";
+            sw.println(
+                "%s { getRequestData().setNamedParameter(\"%s\", %s); %s}",
+                setter.getReadableDeclaration(false, false, false, false, true),
+                propertyName, setter.getParameters()[0].getName(), maybeReturn);
+          }
+        }
+
         // end class X{}
         sw.outdent();
         sw.println("}");
 
         // Instantiate, enqueue, and return
         sw.println("X x = new X();");
+
+        if (request.getApiVersion() != null) {
+          sw.println("x.getRequestData().setApiVersion(\"%s\");",
+              Generator.escape(request.getApiVersion()));
+        }
+
+        // JSON-RPC payloads send their parameters in a by-name fashion
+        if (method.getDialect().equals(Dialect.JSON_RPC)) {
+          for (JParameter param : jmethod.getParameters()) {
+            PropertyName annotation = param.getAnnotation(PropertyName.class);
+            String propertyName = annotation == null ? param.getName()
+                : annotation.value();
+            boolean isContent = param.isAnnotationPresent(JsonRpcContent.class);
+            if (isContent) {
+              sw.println("x.getRequestData().setRequestContent(%s);",
+                  param.getName());
+            } else {
+              sw.println("x.getRequestData().setNamedParameter(\"%s\", %s);",
+                  propertyName, param.getName());
+            }
+          }
+        }
+
         // See comment in AbstractRequest.using(EntityProxy)
         if (!request.isInstance()) {
           sw.println("addInvocation(x);");
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
index 9922620..ae6ce92 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
 
 import java.util.Collections;
 import java.util.List;
@@ -47,6 +49,8 @@
       toReturn.packageName = returnClass.getPackage().getName();
       toReturn.simpleSourceName = returnClass.getName().replace('.', '_')
           + "Impl";
+      toReturn.dialect = returnClass.isAnnotationPresent(JsonRpcService.class)
+          ? Dialect.JSON_RPC : Dialect.STANDARD;
     }
 
     public void setRequestMethods(List<RequestMethod> requestMethods) {
@@ -54,6 +58,7 @@
     }
   }
 
+  private Dialect dialect;
   private String interfaceName;
   private String methodName;
   private String packageName;
@@ -63,6 +68,10 @@
   private ContextMethod() {
   }
 
+  public Dialect getDialect() {
+    return dialect;
+  }
+
   /**
    * The qualified source name of the RequestContext sub-interface (i.e., the
    * return type of the method declaration).
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
index 1e3419b..d634624 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
@@ -16,6 +16,7 @@
 package com.google.gwt.requestfactory.rebind.model;
 
 import com.google.gwt.autobean.rebind.model.JBeanMethod;
+import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
@@ -29,6 +30,8 @@
 import com.google.gwt.requestfactory.rebind.model.RequestMethod.CollectionType;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.InstanceRequest;
+import com.google.gwt.requestfactory.shared.JsonRpcProxy;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
 import com.google.gwt.requestfactory.shared.ProxyFor;
 import com.google.gwt.requestfactory.shared.ProxyForName;
 import com.google.gwt.requestfactory.shared.Request;
@@ -60,6 +63,10 @@
         instanceRequestInterface.getSimpleSourceName());
   }
 
+  static String noSettersAllowed(JMethod found) {
+    return String.format("Optional setters not allowed here: ", found.getName());
+  }
+
   static String poisonedMessage() {
     return "Unable to create RequestFactoryModel model due to previous errors";
   }
@@ -71,6 +78,7 @@
   private final JClassType instanceRequestInterface;
   private final JClassType listInterface;
   private final TreeLogger logger;
+  private final JClassType mapInterface;
   private final TypeOracle oracle;
   /**
    * This map prevents cyclic type dependencies from overflowing the stack.
@@ -81,10 +89,12 @@
    */
   private final Map<JClassType, EntityProxyModel> peers = new LinkedHashMap<JClassType, EntityProxyModel>();
   private boolean poisoned;
-  private final JClassType setInterface;
   private final JClassType requestContextInterface;
   private final JClassType requestFactoryInterface;
   private final JClassType requestInterface;
+  private final JClassType setInterface;
+  private final JClassType splittableType;
+
   private final JClassType valueProxyInterface;
 
   public RequestFactoryModel(TreeLogger logger, JClassType factoryType)
@@ -96,10 +106,12 @@
     entityProxyInterface = oracle.findType(EntityProxy.class.getCanonicalName());
     instanceRequestInterface = oracle.findType(InstanceRequest.class.getCanonicalName());
     listInterface = oracle.findType(List.class.getCanonicalName());
-    setInterface = oracle.findType(Set.class.getCanonicalName());
+    mapInterface = oracle.findType(Map.class.getCanonicalName());
     requestContextInterface = oracle.findType(RequestContext.class.getCanonicalName());
     requestFactoryInterface = oracle.findType(RequestFactory.class.getCanonicalName());
     requestInterface = oracle.findType(Request.class.getCanonicalName());
+    setInterface = oracle.findType(Set.class.getCanonicalName());
+    splittableType = oracle.findType(Splittable.class.getCanonicalName());
     valueProxyInterface = oracle.findType(ValueProxy.class.getCanonicalName());
 
     for (JMethod method : factoryType.getOverridableMethods()) {
@@ -165,9 +177,12 @@
       JClassType contextType) throws UnableToCompleteException {
     Service serviceAnnotation = contextType.getAnnotation(Service.class);
     ServiceName serviceNameAnnotation = contextType.getAnnotation(ServiceName.class);
-    if (serviceAnnotation == null && serviceNameAnnotation == null) {
-      poison("RequestContext subtype %s is missing a @%s annotation",
-          contextType.getQualifiedSourceName(), Service.class.getSimpleName());
+    JsonRpcService jsonRpcAnnotation = contextType.getAnnotation(JsonRpcService.class);
+    if (serviceAnnotation == null && serviceNameAnnotation == null
+        && jsonRpcAnnotation == null) {
+      poison("RequestContext subtype %s is missing a @%s or @%s annotation",
+          contextType.getQualifiedSourceName(), Service.class.getSimpleName(),
+          JsonRpcService.class.getSimpleName());
       return;
     }
 
@@ -181,7 +196,8 @@
       RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
       methodBuilder.setDeclarationMethod(method);
 
-      if (!validateContextMethodAndSetDataType(methodBuilder, method)) {
+      if (!validateContextMethodAndSetDataType(methodBuilder, method,
+          jsonRpcAnnotation != null)) {
         continue;
       }
 
@@ -227,12 +243,12 @@
       // Get the server domain object type
       ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class);
       ProxyForName proxyForName = entityProxyType.getAnnotation(ProxyForName.class);
-      if (proxyFor == null && proxyForName == null) {
-        poison("The %s type does not have a @%s or @%s annotation",
+      JsonRpcProxy jsonRpcProxy = entityProxyType.getAnnotation(JsonRpcProxy.class);
+      if (proxyFor == null && proxyForName == null && jsonRpcProxy == null) {
+        poison("The %s type does not have a @%s, @%s, or @%s annotation",
             entityProxyType.getQualifiedSourceName(),
-            ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName());
-        // early exit, because further processing causes NPEs in numerous spots
-        die(poisonedMessage());
+            ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName(),
+            JsonRpcProxy.class.getSimpleName());
       }
 
       // Look at the methods declared on the EntityProxy
@@ -259,7 +275,8 @@
                 propertyName, previouslySeen.getName(), method.getName());
           }
 
-        } else if (JBeanMethod.SET.matches(method)) {
+        } else if (JBeanMethod.SET.matches(method)
+            || JBeanMethod.SET_BUILDER.matches(method)) {
           transportedType = method.getParameters()[0].getType();
 
         } else if (name.equals("stableId")
@@ -293,7 +310,7 @@
    * Examine a RequestContext method to see if it returns a transportable type.
    */
   private boolean validateContextMethodAndSetDataType(
-      RequestMethod.Builder methodBuilder, JMethod method)
+      RequestMethod.Builder methodBuilder, JMethod method, boolean allowSetters)
       throws UnableToCompleteException {
     JClassType requestReturnType = method.getReturnType().isInterface();
     JClassType invocationReturnType;
@@ -333,6 +350,17 @@
           && paramsOk;
     }
 
+    // Validate any extra properties on the request type
+    for (JMethod maybeSetter : requestReturnType.getInheritableMethods()) {
+      if (JBeanMethod.SET.matches(maybeSetter)
+          || JBeanMethod.SET_BUILDER.matches(maybeSetter)) {
+        if (allowSetters) {
+          methodBuilder.addExtraSetter(maybeSetter);
+        } else {
+          poison(noSettersAllowed(maybeSetter));
+        }
+      }
+    }
     return validateTransportableType(methodBuilder, invocationReturnType, true);
   }
 
@@ -354,7 +382,8 @@
       }
     }
 
-    if (ModelUtils.isValueType(oracle, transportedClass)) {
+    if (ModelUtils.isValueType(oracle, transportedClass)
+        || splittableType.equals(transportedClass)) {
       // Simple values, like Integer and String
       methodBuilder.setValueType(true);
     } else if (entityProxyInterface.isAssignableFrom(transportedClass)
@@ -383,6 +412,28 @@
           collectionInterface, transportedClass)[0];
       methodBuilder.setCollectionElementType(elementType);
       validateTransportableType(methodBuilder, elementType, requireObject);
+    } else if (mapInterface.isAssignableFrom(transportedClass)) {
+      JParameterizedType parameterized = transportedClass.isParameterized();
+      if (parameterized == null) {
+        poison("Requests that return Maps must be parameterized");
+        return false;
+      }
+      if (mapInterface.equals(parameterized.getBaseType())) {
+        methodBuilder.setCollectionType(CollectionType.MAP);
+      } else {
+        poison("Requests that return maps may be declared with" + " %s only",
+            mapInterface.getQualifiedSourceName());
+        return false;
+      }
+      // Also record the element type in the method builder
+      JClassType[] params = ModelUtils.findParameterizationOf(mapInterface,
+          transportedClass);
+      JClassType keyType = params[0];
+      JClassType valueType = params[1];
+      methodBuilder.setMapKeyType(keyType);
+      methodBuilder.setMapValueType(valueType);
+      validateTransportableType(methodBuilder, keyType, requireObject);
+      validateTransportableType(methodBuilder, valueType, requireObject);
     } else {
       // Unknown type, fail
       poison("Invalid Request parameterization %s",
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
index f2dda1e..0761bcf 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
@@ -17,6 +17,11 @@
 
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.requestfactory.shared.JsonRpcWireName;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Represents a method declaration that causes data to be transported. This can
@@ -31,7 +36,19 @@
   public static class Builder {
     private RequestMethod toReturn = new RequestMethod();
 
+    public void addExtraSetter(JMethod method) {
+      if (toReturn.extraSetters == null) {
+        toReturn.extraSetters = new ArrayList<JMethod>();
+      }
+      toReturn.extraSetters.add(method);
+    }
+
     public RequestMethod build() {
+      if (toReturn.extraSetters == null) {
+        toReturn.extraSetters = Collections.emptyList();
+      } else {
+        toReturn.extraSetters = Collections.unmodifiableList(toReturn.extraSetters);
+      }
       try {
         return toReturn;
       } finally {
@@ -53,6 +70,17 @@
 
     public void setDeclarationMethod(JMethod declarationMethod) {
       toReturn.declarationMethod = declarationMethod;
+
+      JClassType returnClass = declarationMethod.getReturnType().isClassOrInterface();
+      JsonRpcWireName annotation = returnClass == null ? null
+          : returnClass.getAnnotation(JsonRpcWireName.class);
+      if (annotation == null) {
+        toReturn.operation = declarationMethod.getEnclosingType().getQualifiedBinaryName()
+            + "::" + declarationMethod.getName();
+      } else {
+        toReturn.operation = annotation.value();
+        toReturn.apiVersion = annotation.version();
+      }
     }
 
     public void setEntityType(EntityProxyModel entityType) {
@@ -63,6 +91,14 @@
       toReturn.instanceType = instanceType;
     }
 
+    public void setMapKeyType(JClassType elementType) {
+      toReturn.mapKeyType = elementType;
+    }
+
+    public void setMapValueType(JClassType elementType) {
+      toReturn.mapValueType = elementType;
+    }
+
     public void setValueType(boolean valueType) {
       toReturn.valueType = valueType;
     }
@@ -72,21 +108,29 @@
    * Indicates the type of collection that a Request will return.
    */
   public enum CollectionType {
-    // NB: Intended to be extended with a MAP value
-    LIST, SET
+    LIST, SET, MAP
   }
 
+  private String apiVersion;
   private JClassType collectionElementType;
   private CollectionType collectionType;
+  private JClassType dataType;
   private JMethod declarationMethod;
   private EntityProxyModel entityType;
+  private List<JMethod> extraSetters = new ArrayList<JMethod>();
   private EntityProxyModel instanceType;
-  private JClassType dataType;
+  private String operation;
+  private JClassType mapValueType;
+  private JClassType mapKeyType;
   private boolean valueType;
 
   private RequestMethod() {
   }
 
+  public String getApiVersion() {
+    return apiVersion;
+  }
+
   /**
    * If the method returns a collection, this method will return the element
    * type.
@@ -118,6 +162,10 @@
     return entityType;
   }
 
+  public List<JMethod> getExtraSetters() {
+    return extraSetters;
+  }
+
   /**
    * If the method is intended to be invoked on an instance of an EntityProxy,
    * returns the EntityProxyModel describing that type.
@@ -126,6 +174,18 @@
     return instanceType;
   }
 
+  public JClassType getMapKeyType() {
+    return mapKeyType;
+  }
+
+  public JClassType getMapValueType() {
+    return mapValueType;
+  }
+
+  public String getOperation() {
+    return operation;
+  }
+
   public boolean isCollectionType() {
     return collectionType != null;
   }
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
index a04226b..9445f54 100644
--- a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
+++ b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
@@ -158,7 +158,11 @@
 
   @Override
   public Method getSetter(Class<?> domainType, String property) {
-    return getBeanMethod(BeanMethod.SET, domainType, property);
+    Method setter = getBeanMethod(BeanMethod.SET, domainType, property);
+    if (setter == null) {
+      setter = getBeanMethod(BeanMethod.SET_BUILDER, domainType, property);
+    }
+    return setter;
   }
 
   @Override
diff --git a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
index 2f02c01..0691717 100644
--- a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
@@ -15,8 +15,12 @@
  */
 package com.google.gwt.requestfactory.server.testing;
 
+import com.google.gwt.autobean.server.impl.BeanMethod;
 import com.google.gwt.autobean.server.impl.TypeUtils;
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
 import com.google.gwt.requestfactory.shared.InstanceRequest;
+import com.google.gwt.requestfactory.shared.JsonRpcContent;
+import com.google.gwt.requestfactory.shared.JsonRpcWireName;
 import com.google.gwt.requestfactory.shared.Request;
 import com.google.gwt.requestfactory.shared.RequestContext;
 import com.google.gwt.requestfactory.shared.impl.AbstractRequest;
@@ -24,9 +28,11 @@
 import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
 import com.google.gwt.requestfactory.shared.impl.RequestData;
 
+import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
 import java.lang.reflect.Type;
 import java.util.Collection;
 
@@ -34,10 +40,8 @@
  * An in-process implementation of RequestContext
  */
 class InProcessRequestContext extends AbstractRequestContext {
-  static final Object[] NO_ARGS = new Object[0];
-
   class RequestContextHandler implements InvocationHandler {
-    public Object invoke(Object proxy, Method method, Object[] args)
+    public Object invoke(Object proxy, Method method, final Object[] args)
         throws Throwable {
       // Maybe delegate to superclass
       Class<?> owner = method.getDeclaringClass();
@@ -78,21 +82,63 @@
         }
       }
 
-      // Calculate request metadata
-      final String operation = method.getDeclaringClass().getName() + "::"
-          + method.getName();
-      final Class<?> returnType = TypeUtils.ensureBaseType(returnGenericType);
-      final Class<?> elementType = Collection.class.isAssignableFrom(returnType)
+      Class<?> returnType = TypeUtils.ensureBaseType(returnGenericType);
+      Class<?> elementType = Collection.class.isAssignableFrom(returnType)
           ? TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
               Collection.class, returnGenericType)) : null;
 
+      final RequestData data;
+      if (dialect.equals(Dialect.STANDARD)) {
+        String operation = method.getDeclaringClass().getName() + "::"
+            + method.getName();
+
+        data = new RequestData(operation, actualArgs, returnType, elementType);
+      } else {
+        // Calculate request metadata
+        JsonRpcWireName wireInfo = method.getReturnType().getAnnotation(
+            JsonRpcWireName.class);
+        String apiVersion = wireInfo.version();
+        String operation = wireInfo.value();
+
+        int foundContent = -1;
+        final String[] parameterNames = args == null ? new String[0]
+            : new String[args.length];
+        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+        parameter : for (int i = 0, j = parameterAnnotations.length; i < j; i++) {
+          for (Annotation annotation : parameterAnnotations[i]) {
+            if (PropertyName.class.equals(annotation.annotationType())) {
+              parameterNames[i] = ((PropertyName) annotation).value();
+              continue parameter;
+            } else if (JsonRpcContent.class.equals(annotation.annotationType())) {
+              foundContent = i;
+              continue parameter;
+            }
+          }
+          throw new UnsupportedOperationException("No "
+              + PropertyName.class.getCanonicalName()
+              + " annotation on parameter " + i + " of method "
+              + method.toString());
+        }
+        final int contentIdx = foundContent;
+
+        data = new RequestData(operation, actualArgs, returnType, elementType);
+        for (int i = 0, j = args.length; i < j; i++) {
+          if (i != contentIdx) {
+            data.setNamedParameter(parameterNames[i], args[i]);
+          } else {
+            data.setRequestContent(args[i]);
+          }
+          data.setApiVersion(apiVersion);
+        }
+      }
+
       // Create the request, just filling in the RequestData details
-      AbstractRequest<Object> req = new AbstractRequest<Object>(
+      final AbstractRequest<Object> req = new AbstractRequest<Object>(
           InProcessRequestContext.this) {
         @Override
         protected RequestData makeRequestData() {
-          return new RequestData(operation, actualArgs, propertyRefs,
-              returnType, elementType);
+          data.setPropertyRefs(propertyRefs);
+          return data;
         }
       };
 
@@ -100,11 +146,42 @@
         // Instance invocations are enqueued when using() is called
         addInvocation(req);
       }
-      return req;
-    }
-  };
 
-  protected InProcessRequestContext(AbstractRequestFactory factory) {
-    super(factory);
+      if (dialect.equals(Dialect.STANDARD)) {
+        return req;
+      } else if (dialect.equals(Dialect.JSON_RPC)) {
+        // Support optional parameters for JSON-RPC payloads
+        Class<?> requestType = method.getReturnType().asSubclass(Request.class);
+        return Proxy.newProxyInstance(requestType.getClassLoader(),
+            new Class<?>[] {requestType}, new InvocationHandler() {
+              @Override
+              public Object invoke(Object proxy, Method method, Object[] args)
+                  throws Throwable {
+                if (Object.class.equals(method.getDeclaringClass())
+                    || Request.class.equals(method.getDeclaringClass())) {
+                  return method.invoke(req, args);
+                } else if (BeanMethod.SET.matches(method)
+                    || BeanMethod.SET_BUILDER.matches(method)) {
+                  req.getRequestData().setNamedParameter(
+                      BeanMethod.SET.inferName(method), args[0]);
+                  return Void.TYPE.equals(method.getReturnType()) ? null
+                      : proxy;
+                }
+                throw new UnsupportedOperationException(method.toString());
+              }
+            });
+      } else {
+        throw new RuntimeException("Should not reach here");
+      }
+    }
+  }
+
+  static final Object[] NO_ARGS = new Object[0];
+  private final Dialect dialect;
+
+  protected InProcessRequestContext(AbstractRequestFactory factory,
+      Dialect dialect) {
+    super(factory, dialect);
+    this.dialect = dialect;
   }
 }
diff --git a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
index af61538..836fb1a 100644
--- a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
@@ -24,9 +24,11 @@
 import com.google.gwt.requestfactory.shared.BaseProxy;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
 import com.google.gwt.requestfactory.shared.RequestContext;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.ValueProxy;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
 import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
 import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
 import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory;
@@ -62,8 +64,10 @@
 
       Class<? extends RequestContext> context = method.getReturnType().asSubclass(
           RequestContext.class);
+      Dialect dialect = method.getReturnType().isAnnotationPresent(
+          JsonRpcService.class) ? Dialect.JSON_RPC : Dialect.STANDARD;
       RequestContextHandler handler = new InProcessRequestContext(
-          InProcessRequestFactory.this).new RequestContextHandler();
+          InProcessRequestFactory.this, dialect).new RequestContextHandler();
       return context.cast(Proxy.newProxyInstance(
           Thread.currentThread().getContextClassLoader(),
           new Class<?>[] {context}, handler));
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java
new file mode 100644
index 0000000..7fe081e
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java
@@ -0,0 +1,32 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <b>Experimental API, subject to change.</b> Applied to a Request method
+ * declaration to indicate that a particular parameter is used as the
+ * {@code request} portion of the JSON-RPC request. This is analogous to the
+ * payload body in a REST-style request.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface JsonRpcContent {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java
new file mode 100644
index 0000000..5cb7300
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java
@@ -0,0 +1,23 @@
+/*
+ * 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.requestfactory.shared;
+
+/**
+ * <b>Experimental API, subject to change</b> Used instead of the
+ * {@link ProxyFor} annotation.
+ */
+public @interface JsonRpcProxy {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java
new file mode 100644
index 0000000..e147b72
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java
@@ -0,0 +1,30 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <b>Experimental API, subject to change</b> Indicates that a RequestContext
+ * should be encoded as a JSON-RPC request.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonRpcService {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java
new file mode 100644
index 0000000..36449c4
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java
@@ -0,0 +1,30 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * <b>Experimental API, subject to change</b> Provides the method name for a
+ * JSON-RPC invocation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface JsonRpcWireName {
+  String value();
+
+  String version() default "";
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
index 2a20f06..8524524 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
@@ -83,7 +83,7 @@
    * methods place the instance in the first parameter slot.
    */
   public Request<T> using(BaseProxy instanceObject) {
-    getRequestData().getParameters()[0] = instanceObject;
+    getRequestData().getOrderedParameters()[0] = instanceObject;
     /*
      * Instance methods enqueue themselves when their using() method is called.
      * This ensures that the instance parameter will have been set when
@@ -100,6 +100,10 @@
 
   protected abstract RequestData makeRequestData();
 
+  Receiver<? super T> getReceiver() {
+    return receiver;
+  }
+
   boolean hasReceiver() {
     return receiver != null;
   }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
index ea8dff2..7e89967 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -24,6 +24,9 @@
 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.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.gwt.requestfactory.shared.BaseProxy;
 import com.google.gwt.requestfactory.shared.EntityProxy;
@@ -40,6 +43,7 @@
 import com.google.gwt.requestfactory.shared.messages.IdMessage;
 import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
 import com.google.gwt.requestfactory.shared.messages.InvocationMessage;
+import com.google.gwt.requestfactory.shared.messages.JsonRpcRequest;
 import com.google.gwt.requestfactory.shared.messages.MessageFactory;
 import com.google.gwt.requestfactory.shared.messages.OperationMessage;
 import com.google.gwt.requestfactory.shared.messages.RequestMessage;
@@ -63,6 +67,238 @@
  */
 public 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
+   * object is instantiated.
+   */
+  public enum Dialect {
+    STANDARD {
+      @Override
+      DialectImpl create(AbstractRequestContext context) {
+        return context.new StandardPayloadDialect();
+      }
+    },
+    JSON_RPC {
+      @Override
+      DialectImpl create(AbstractRequestContext context) {
+        return context.new JsonRpcPayloadDialect();
+      }
+    };
+    abstract DialectImpl create(AbstractRequestContext context);
+  }
+
+  interface DialectImpl {
+
+    void addInvocation(AbstractRequest<?> request);
+
+    String makePayload();
+
+    void processPayload(Receiver<Void> receiver, String payload);
+  }
+
+  class JsonRpcPayloadDialect implements DialectImpl {
+    /**
+     * Called by generated subclasses to enqueue a method invocation.
+     */
+    public void addInvocation(AbstractRequest<?> request) {
+      /*
+       * TODO(bobv): Support for multiple invocations per request needs to be
+       * ironed out. Once this is done, addInvocation() can be removed from the
+       * DialectImpl interface and restored to to AbstractRequestContext.
+       */
+      if (!invocations.isEmpty()) {
+        throw new RuntimeException(
+            "Only one invocation per request, pending backend support");
+      }
+      invocations.add(request);
+      for (Object arg : request.getRequestData().getOrderedParameters()) {
+        retainArg(arg);
+      }
+    }
+
+    public String makePayload() {
+      RequestData data = invocations.get(0).getRequestData();
+
+      AutoBean<JsonRpcRequest> bean = MessageFactoryHolder.FACTORY.jsonRpcRequest();
+      JsonRpcRequest request = bean.as();
+
+      request.setVersion("2.0");
+      request.setApiVersion(data.getApiVersion());
+      request.setId(payloadId++);
+
+      Map<String, Splittable> params = new HashMap<String, Splittable>();
+      for (Map.Entry<String, Object> entry : data.getNamedParameters().entrySet()) {
+        Object obj = entry.getValue();
+        Splittable value = encode(obj);
+        params.put(entry.getKey(), value);
+      }
+      if (data.getRequestResource() != null) {
+        params.put("resource", encode(data.getRequestResource()));
+      }
+      request.setParams(params);
+      request.setMethod(data.getOperation());
+
+      return AutoBeanCodex.encode(bean).getPayload();
+    }
+
+    public void processPayload(Receiver<Void> receiver, String payload) {
+      Splittable raw = StringQuoter.split(payload);
+
+      @SuppressWarnings("unchecked")
+      Receiver<Object> callback = (Receiver<Object>) invocations.get(0).getReceiver();
+
+      if (!raw.isNull("error")) {
+        Splittable error = raw.get("error");
+        ServerFailure failure = new ServerFailure(
+            error.get("message").asString());
+        fail(receiver, failure);
+        return;
+      }
+
+      Splittable result = raw.get("result");
+      @SuppressWarnings("unchecked")
+      Class<BaseProxy> target = (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType();
+
+      SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target);
+      AutoBean<BaseProxy> bean = getRequestFactory().createProxy(target, id);
+      AutoBeanCodex.decodeInto(result, bean);
+
+      if (callback != null) {
+        callback.onSuccess(bean.as());
+      }
+      if (receiver != null) {
+        receiver.onSuccess(null);
+      }
+    }
+
+    Splittable encode(Object obj) {
+      Splittable value;
+      if (obj == null) {
+        return LazySplittable.NULL;
+      } else if (obj.getClass().isEnum()
+          && getRequestFactory() instanceof EnumMap) {
+        value = ValueCodex.encode(((EnumMap) getRequestFactory()).getToken((Enum<?>) obj));
+      } else if (ValueCodex.canDecode(obj.getClass())) {
+        value = ValueCodex.encode(obj);
+      } else {
+        // XXX user-provided implementation of interface?
+        value = AutoBeanCodex.encode(AutoBeanUtils.getAutoBean(obj));
+      }
+      return value;
+    }
+  }
+
+  class StandardPayloadDialect implements DialectImpl {
+
+    /**
+     * Called by generated subclasses to enqueue a method invocation.
+     */
+    public void addInvocation(AbstractRequest<?> request) {
+      invocations.add(request);
+      for (Object arg : request.getRequestData().getOrderedParameters()) {
+        retainArg(arg);
+      }
+    }
+
+    /**
+     * Assemble all of the state that has been accumulated in this context. This
+     * includes:
+     * <ul>
+     * <li>Diffs accumulated on objects passed to {@link #edit}.
+     * <li>Invocations accumulated as Request subtypes passed to
+     * {@link #addInvocation}.
+     * </ul>
+     */
+    public String makePayload() {
+      // Get the factory from the runtime-specific holder.
+      MessageFactory f = MessageFactoryHolder.FACTORY;
+
+      List<OperationMessage> operations = makePayloadOperations();
+      List<InvocationMessage> invocationMessages = makePayloadInvocations();
+
+      // Create the outer envelope message
+      AutoBean<RequestMessage> bean = f.request();
+      RequestMessage requestMessage = bean.as();
+      if (!invocationMessages.isEmpty()) {
+        requestMessage.setInvocations(invocationMessages);
+      }
+      if (!operations.isEmpty()) {
+        requestMessage.setOperations(operations);
+      }
+      return AutoBeanCodex.encode(bean).getPayload();
+    }
+
+    public void processPayload(final Receiver<Void> receiver, String payload) {
+      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());
+
+        fail(receiver, fail);
+        return;
+      }
+
+      // Process violations and then stop
+      if (response.getViolations() != null) {
+        Set<Violation> errors = new HashSet<Violation>();
+        for (ViolationMessage message : response.getViolations()) {
+          errors.add(new MyViolation(message));
+        }
+
+        violation(receiver, errors);
+        return;
+      }
+
+      // Process operations
+      processReturnOperations(response);
+
+      // Send return values
+      Set<Throwable> causes = null;
+      for (int i = 0, j = invocations.size(); i < j; i++) {
+        try {
+          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();
+            invocations.get(i).onFail(
+                new ServerFailure(failure.getMessage(),
+                    failure.getExceptionType(), failure.getStackTrace(),
+                    failure.isFatal()));
+          }
+        } catch (Throwable t) {
+          if (causes == null) {
+            causes = new HashSet<Throwable>();
+          }
+          causes.add(t);
+        }
+      }
+
+      if (receiver != null) {
+        try {
+          receiver.onSuccess(null);
+        } catch (Throwable t) {
+          if (causes == null) {
+            causes = new HashSet<Throwable>();
+          }
+          causes.add(t);
+        }
+      }
+      // After success, shut down the context
+      editedProxies.clear();
+      invocations.clear();
+      returnedProxies.clear();
+
+      if (causes != null) {
+        throw new UmbrellaException(causes);
+      }
+    }
+  }
   private class MyViolation implements Violation {
 
     private final BaseProxy currentProxy;
@@ -116,18 +352,21 @@
 
   private static final WriteOperation[] DELETE_ONLY = {WriteOperation.DELETE};
   private static final WriteOperation[] PERSIST_AND_UPDATE = {
-    WriteOperation.PERSIST, WriteOperation.UPDATE};
+      WriteOperation.PERSIST, WriteOperation.UPDATE};
   private static final WriteOperation[] UPDATE_ONLY = {WriteOperation.UPDATE};
+  private static int payloadId = 100;
 
-  private final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
+  protected final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
   private boolean locked;
   private final AbstractRequestFactory requestFactory;
+
   /**
    * A map of all EntityProxies that the RequestContext has interacted with.
    * 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>>();
+
   /**
    * A map that contains the canonical instance of an entity to return in the
    * return graph, since this is built from scratch.
@@ -142,8 +381,12 @@
    */
   private final Map<Integer, SimpleProxyId<?>> syntheticIds = new HashMap<Integer, SimpleProxyId<?>>();
 
-  protected AbstractRequestContext(AbstractRequestFactory factory) {
+  private final DialectImpl dialect;
+
+  protected AbstractRequestContext(AbstractRequestFactory factory,
+      Dialect dialect) {
     this.requestFactory = factory;
+    this.dialect = dialect.create(this);
   }
 
   /**
@@ -291,15 +534,80 @@
    */
   public boolean isValueType(Class<?> clazz) {
     return requestFactory.isValueType(clazz);
-  }
+  };
 
   /**
    * Called by generated subclasses to enqueue a method invocation.
    */
   protected void addInvocation(AbstractRequest<?> request) {
-    invocations.add(request);
-    for (Object arg : request.getRequestData().getParameters()) {
-      retainArg(arg);
+    dialect.addInvocation(request);
+  }
+
+  /**
+   * Invoke the appropriate {@code onFailure} callbacks, possibly throwing an
+   * {@link UmbrellaException} if one or more callbacks fails.
+   */
+  protected void fail(Receiver<Void> receiver, ServerFailure failure) {
+    reuse();
+    Set<Throwable> causes = null;
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
+        invocations)) {
+      try {
+        request.onFail(failure);
+      } catch (Throwable t) {
+        if (causes == null) {
+          causes = new HashSet<Throwable>();
+        }
+        causes.add(t);
+      }
+    }
+    if (receiver != null) {
+      try {
+        receiver.onFailure(failure);
+      } catch (Throwable t) {
+        if (causes == null) {
+          causes = new HashSet<Throwable>();
+        }
+        causes.add(t);
+      }
+    }
+
+    if (causes != null) {
+      throw new UmbrellaException(causes);
+    }
+  }
+
+  /**
+   * Invoke the appropriate {@code onViolation} callbacks, possibly throwing an
+   * {@link UmbrellaException} if one or more callbacks fails.
+   */
+  protected void violation(final Receiver<Void> receiver, Set<Violation> errors) {
+    reuse();
+    Set<Throwable> causes = null;
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
+        invocations)) {
+      try {
+        request.onViolation(errors);
+      } catch (Throwable t) {
+        if (causes == null) {
+          causes = new HashSet<Throwable>();
+        }
+        causes.add(t);
+      }
+    }
+    if (receiver != null) {
+      try {
+        receiver.onViolation(errors);
+      } catch (Throwable t) {
+        if (causes == null) {
+          causes = new HashSet<Throwable>();
+        }
+        causes.add(t);
+      }
+    }
+
+    if (causes != null) {
+      throw new UmbrellaException(causes);
     }
   }
 
@@ -603,150 +911,14 @@
 
     freezeEntities(true);
 
-    String payload = makePayload();
+    String payload = dialect.makePayload();
     requestFactory.getRequestTransport().send(payload, new TransportReceiver() {
       public void onTransportFailure(ServerFailure failure) {
         fail(receiver, failure);
       }
 
       public void onTransportSuccess(String payload) {
-        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());
-
-          fail(receiver, fail);
-          return;
-        }
-
-        // Process violations and then stop
-        if (response.getViolations() != null) {
-          Set<Violation> errors = new HashSet<Violation>();
-          for (ViolationMessage message : response.getViolations()) {
-            errors.add(new MyViolation(message));
-          }
-
-          violation(receiver, errors);
-          return;
-        }
-
-        // Process operations
-        processReturnOperations(response);
-
-        // Send return values
-        Set<Throwable> causes = null;
-        for (int i = 0, j = invocations.size(); i < j; i++) {
-          try {
-            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();
-              invocations.get(i).onFail(
-                  new ServerFailure(failure.getMessage(),
-                      failure.getExceptionType(), failure.getStackTrace(),
-                      failure.isFatal()));
-            }
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-
-        if (receiver != null) {
-          try {
-            receiver.onSuccess(null);
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-        // After success, shut down the context
-        editedProxies.clear();
-        invocations.clear();
-        returnedProxies.clear();
-
-        if (causes != null) {
-          throw new UmbrellaException(causes);
-        }
-      }
-
-      /**
-       * Invoke the appropriate {@code onFailure} callbacks, possibly throwing
-       * an {@link UmbrellaException} if one or more callbacks fails.
-       */
-      private void fail(Receiver<Void> receiver, ServerFailure failure) {
-        reuse();
-        Set<Throwable> causes = null;
-        for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
-            invocations)) {
-          try {
-            request.onFail(failure);
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-        if (receiver != null) {
-          try {
-            receiver.onFailure(failure);
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-
-        if (causes != null) {
-          throw new UmbrellaException(causes);
-        }
-      }
-
-      /**
-       * Invoke the appropriate {@code onViolation} callbacks, possibly throwing
-       * an {@link UmbrellaException} if one or more callbacks fails.
-       */
-      private void violation(final Receiver<Void> receiver,
-          Set<Violation> errors) {
-        reuse();
-        Set<Throwable> causes = null;
-        for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
-            invocations)) {
-          try {
-            request.onViolation(errors);
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-        if (receiver != null) {
-          try {
-            receiver.onViolation(errors);
-          } catch (Throwable t) {
-            if (causes == null) {
-              causes = new HashSet<Throwable>();
-            }
-            causes.add(t);
-          }
-        }
-
-        if (causes != null) {
-          throw new UmbrellaException(causes);
-        }
+        dialect.processPayload(receiver, payload);
       }
     });
   }
@@ -772,34 +944,6 @@
   }
 
   /**
-   * Assemble all of the state that has been accumulated in this context. This
-   * includes:
-   * <ul>
-   * <li>Diffs accumulated on objects passed to {@link #edit}.
-   * <li>Invocations accumulated as Request subtypes passed to
-   * {@link #addInvocation}.
-   * </ul>
-   */
-  private String makePayload() {
-    // Get the factory from the runtime-specific holder.
-    MessageFactory f = MessageFactoryHolder.FACTORY;
-
-    List<OperationMessage> operations = makePayloadOperations();
-    List<InvocationMessage> invocationMessages = makePayloadInvocations();
-
-    // Create the outer envelope message
-    AutoBean<RequestMessage> bean = f.request();
-    RequestMessage requestMessage = bean.as();
-    if (!invocationMessages.isEmpty()) {
-      requestMessage.setInvocations(invocationMessages);
-    }
-    if (!operations.isEmpty()) {
-      requestMessage.setOperations(operations);
-    }
-    return AutoBeanCodex.encode(bean).getPayload();
-  }
-
-  /**
    * Create an InvocationMessage for each remote method call being made by the
    * context.
    */
@@ -823,8 +967,8 @@
 
       // Parameter values or references
       List<Splittable> parameters = new ArrayList<Splittable>(
-          data.getParameters().length);
-      for (Object param : data.getParameters()) {
+          data.getOrderedParameters().length);
+      for (Object param : data.getOrderedParameters()) {
         parameters.add(EntityCodex.encode(this, param));
       }
       if (!parameters.isEmpty()) {
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
index 9616776..25c202b 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -72,7 +72,7 @@
     }
 
     AbstractRequestContext context = new AbstractRequestContext(
-        AbstractRequestFactory.this);
+        AbstractRequestFactory.this, AbstractRequestContext.Dialect.STANDARD);
     return new AbstractRequest<P>(context) {
       {
         requestContext.addInvocation(this);
@@ -82,7 +82,7 @@
       protected RequestData makeRequestData() {
         return new RequestData(
             "com.google.gwt.requestfactory.shared.impl.FindRequest::find",
-            new Object[]{proxyId}, propertyRefs, proxyId.getProxyClass(), null);
+            new Object[] {proxyId}, propertyRefs, proxyId.getProxyClass(), null);
       }
     };
   }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
index 73b9c55..7e175c2 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -63,7 +63,7 @@
   private final Map<SimpleProxyId<?>, AutoBean<?>> serialized = new HashMap<SimpleProxyId<?>, AutoBean<?>>();
 
   public ProxySerializerImpl(AbstractRequestFactory factory, ProxyStore store) {
-    super(factory);
+    super(factory, Dialect.STANDARD);
     this.store = store;
   }
 
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
index cb9c659..db66e2c 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
@@ -15,6 +15,9 @@
  */
 package com.google.gwt.requestfactory.shared.impl;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -25,34 +28,54 @@
   private final Class<?> elementType;
   private final String operation;
   private final Object[] parameters;
-  private final Set<String> propertyRefs;
+  private Set<String> propertyRefs;
   private final Class<?> returnType;
+  private Map<String, Object> requestParameters;
+  private Object requestContent;
+  private String apiVersion;
 
   public RequestData(String operation, Object[] parameters,
-      Set<String> propertyRefs, Class<?> returnType, Class<?> elementType) {
+      Class<?> returnType, Class<?> elementType) {
     this.operation = operation;
     this.parameters = parameters;
-    this.propertyRefs = propertyRefs;
     this.returnType = returnType;
     this.elementType = elementType;
   }
 
   /**
+   * Used by generated code.
+   */
+  public RequestData(String operation, Object[] parameters,
+      Set<String> propertyRefs, Class<?> returnType, Class<?> elementType) {
+    this(operation, parameters, returnType, elementType);
+    setPropertyRefs(propertyRefs);
+  }
+
+  public String getApiVersion() {
+    return apiVersion;
+  }
+
+  /**
    * Used to interpret the returned payload.
    */
   public Class<?> getElementType() {
     return elementType;
   }
 
+  public Map<String, Object> getNamedParameters() {
+    return requestParameters == null ? Collections.<String, Object> emptyMap()
+        : requestParameters;
+  }
+
   public String getOperation() {
     return operation;
   }
 
   /**
-   * Used by InstanceRequest subtypes to reset the instance object in the
-   * <code>using</code> method.
+   * Used by standard-mode payloads and InstanceRequest subtypes to reset the
+   * instance object in the <code>using</code> method.
    */
-  public Object[] getParameters() {
+  public Object[] getOrderedParameters() {
     return parameters;
   }
 
@@ -60,10 +83,38 @@
     return propertyRefs;
   }
 
+  public Object getRequestResource() {
+    return requestContent;
+  }
+
   /**
    * Used to interpret the returned payload.
    */
   public Class<?> getReturnType() {
     return returnType;
   }
+
+  public void setApiVersion(String apiVersion) {
+    this.apiVersion = apiVersion;
+  }
+
+  public void setNamedParameter(String key, Object value) {
+    if (requestParameters == null) {
+      requestParameters = new HashMap<String, Object>();
+    }
+    requestParameters.put(key, value);
+  }
+
+  public void setPropertyRefs(Set<String> propertyRefs) {
+    this.propertyRefs = propertyRefs;
+  }
+
+  /**
+   * Represents the {@code request} object in a JSON-RPC request.
+   * 
+   * @see com.google.gwt.requestfactory.shared.JsonRpcContent
+   */
+  public void setRequestContent(Object requestContent) {
+    this.requestContent = requestContent;
+  }
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java b/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java
new file mode 100644
index 0000000..063f315
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.requestfactory.shared.messages;
+
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
+import com.google.gwt.autobean.shared.Splittable;
+
+import java.util.Map;
+
+/**
+ * A JSON-RPC request payload.
+ */
+public interface JsonRpcRequest {
+  String getApiVersion();
+
+  int getId();
+
+  String getMethod();
+
+  Map<String, Splittable> getParams();
+
+  @PropertyName("jsonrpc")
+  String getVersion();
+
+  void setApiVersion(String version);
+
+  void setId(int id);
+
+  void setMethod(String method);
+
+  void setParams(Map<String, Splittable> params);
+
+  @PropertyName("jsonrpc")
+  void setVersion(String version);
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
index a6201f5..c38216e 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
@@ -27,6 +27,8 @@
   AutoBean<IdMessage> id();
 
   AutoBean<InvocationMessage> invocation();
+  
+  AutoBean<JsonRpcRequest> jsonRpcRequest();
 
   AutoBean<OperationMessage> operation();
 
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 3f1c489..cb331b6 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
@@ -15,11 +15,15 @@
  */
 package com.google.gwt.autobean.shared.impl;
 
-import com.google.gwt.core.client.JsonUtils;
 import com.google.gwt.autobean.client.impl.JsoSplittable;
 import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.core.client.JavaScriptException;
+import com.google.gwt.core.client.JsDate;
+import com.google.gwt.core.client.JsonUtils;
 import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter;
 
+import java.util.Date;
+
 /**
  * This a super-source version with a client-only implementation.
  */
@@ -39,4 +43,17 @@
     }
     return toReturn;
   }
+
+  public static Date tryParseDate(String date) {
+    try {
+      return new Date(Long.parseLong(date));
+    } catch (NumberFormatException ignored) {
+    }
+    try {
+      JsDate js = JsDate.create(date);
+      return new Date((long) js.getTime());
+    } catch (JavaScriptException ignored) {
+    }
+    return null;
+  }
 }
diff --git a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
index 14f14fb..2cb275b 100644
--- a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
+++ b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.requestfactory.rebind.model;
 
+import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.javac.CompilationState;
@@ -148,13 +149,15 @@
   public void testMissingProxyFor() {
     testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
         TestContextImpl.class.getName(), null,
-        "The t.TestProxy type does not have a @ProxyFor or @ProxyForName annotation");
+        "The t.TestProxy type does not have a @ProxyFor, "
+            + "@ProxyForName, or @JsonRpcProxy annotation");
   }
 
   public void testMissingService() {
     testModelWithMethodDeclArgs("Request<String> okMethod();", null,
         TestContextImpl.class.getName(),
-        "RequestContext subtype t.TestContext is missing a @Service annotation");
+        "RequestContext subtype t.TestContext is missing a "
+            + "@Service or @JsonRpcService annotation");
   }
 
   public void testModelWithMethodDecl(final String clientMethodDecls,
@@ -296,6 +299,7 @@
         new EmptyMockJavaResource(RequestFactory.class),
         new EmptyMockJavaResource(Receiver.class),
         new EmptyMockJavaResource(ServiceLocator.class),
+        new EmptyMockJavaResource(Splittable.class),
         new EmptyMockJavaResource(ValueProxy.class),
 
         new RealJavaResource(Request.class),