Rewrote JSON library.

Review by: bruce


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2002 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/json/client/JSONArray.java b/user/src/com/google/gwt/json/client/JSONArray.java
index b9d24a2..97024ba 100644
--- a/user/src/com/google/gwt/json/client/JSONArray.java
+++ b/user/src/com/google/gwt/json/client/JSONArray.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -21,17 +21,22 @@
  * Represents an array of {@link com.google.gwt.json.client.JSONValue} objects.
  */
 public class JSONArray extends JSONValue {
+  
+  /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static JavaScriptObject unwrap(JSONArray value) {
+    return value.jsArray;
+  }
 
-  final JavaScriptObject javascriptArray;
-
-  final JavaScriptObject wrappedArray;
+  private final JavaScriptObject jsArray;
 
   /**
    * Creates an empty JSONArray.
    */
   public JSONArray() {
-    javascriptArray = createArray();
-    wrappedArray = createArray();
+    jsArray = JavaScriptObject.createArray();
   }
 
   /**
@@ -41,8 +46,19 @@
    * @param arr a JavaScript array
    */
   public JSONArray(JavaScriptObject arr) {
-    javascriptArray = arr;
-    wrappedArray = createArray();
+    jsArray = arr;
+  }
+
+  /**
+   * Returns <code>true</code> if <code>other</code> is a {@link JSONArray}
+   * wrapping the same underlying object.
+   */
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof JSONArray)) {
+      return false;
+    }
+    return jsArray.equals(((JSONArray) other).jsArray);
   }
 
   /**
@@ -52,22 +68,22 @@
    * @return the value at this index, or <code>null</code> if this index is
    *         empty
    */
-  public JSONValue get(int index) throws JSONException {
-    if (wrappedTest(index)) {
-      return wrappedGet(index);
-    }
-    JSONValue wrapped = null;
-    if (rawTest(index)) {
-      Object o = rawGet(index);
-      if (o instanceof String) {
-        wrapped = new JSONString((String) o);
-      } else {
-        wrapped = JSONParser.buildValue((JavaScriptObject) o);
-      }
-      rawSet(index, null);
-    }
-    wrappedSet(index, wrapped);
-    return wrapped;
+  public native JSONValue get(int index) /*-{
+    var v = this.@com.google.gwt.json.client.JSONArray::jsArray[index];
+    var func = @com.google.gwt.json.client.JSONParser::typeMap[typeof v];
+    return func ? func(v) : @com.google.gwt.json.client.JSONParser::throwUnknownTypeException(Ljava/lang/String;)(typeof v);
+  }-*/;
+
+  /**
+   * Returns the underlying JavaScript array that this object wraps.
+   */
+  public JavaScriptObject getJavaScriptObject() {
+    return jsArray;
+  }
+
+  @Override
+  public int hashCode() {
+    return jsArray.hashCode();
   }
 
   /**
@@ -82,15 +98,14 @@
    * Sets the specified index to the given value.
    * 
    * @param index the index to set
-   * @param jsonValue the value to set
+   * @param value the value to set
    * @return the previous value at this index, or <code>null</code> if this
    *         index was empty
    */
-  public JSONValue set(int index, JSONValue jsonValue) {
-    JSONValue out = get(index);
-    wrappedSet(index, jsonValue);
-    rawSet(index, null);
-    return out;
+  public JSONValue set(int index, JSONValue value) {
+    JSONValue previous = get(index);
+    set0(index, value);
+    return previous;
   }
 
   /**
@@ -99,7 +114,7 @@
    * @return size of this array
    */
   public native int size() /*-{
-    return this.@com.google.gwt.json.client.JSONArray::javascriptArray.length;
+    return this.@com.google.gwt.json.client.JSONArray::jsArray.length;
   }-*/;
 
   /**
@@ -108,52 +123,31 @@
    * large.
    */
   @Override
-  public String toString() throws JSONException {
+  public String toString() {
     StringBuffer sb = new StringBuffer();
     sb.append("[");
     for (int i = 0, c = size(); i < c; i++) {
-      JSONValue value = get(i);
-      sb.append(value.toString());
-
-      if (i < c - 1) {
+      if (i > 0) {
         sb.append(",");
       }
+      sb.append(get(i));
     }
     sb.append("]");
     return sb.toString();
   }
 
-  private native JavaScriptObject createArray() /*-{
-    return [];
+  @Override
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONArray::unwrap(Lcom/google/gwt/json/client/JSONArray;);
   }-*/;
 
-  private native Object rawGet(int index) /*-{
-    var x = this.@com.google.gwt.json.client.JSONArray::javascriptArray[index];
-    if (typeof x == 'number' || typeof x == 'array' || typeof x == 'boolean') {
-      x = (Object(x));
+  private native void set0(int index, JSONValue value) /*-{
+    if (value === null) {
+      value = undefined;
+    } else {
+      var func = value.@com.google.gwt.json.client.JSONValue::getUnwrapper()();
+      value = func(value);
     }
-    return x;
-  }-*/;
-
-  private native void rawSet(int index, Object value) /*-{
-    this.@com.google.gwt.json.client.JSONArray::javascriptArray[index] = value;
-  }-*/;
-
-  private native boolean rawTest(int index) /*-{
-    var x = this.@com.google.gwt.json.client.JSONArray::javascriptArray[index];
-    return x !== undefined;
-  }-*/;
-
-  private native JSONValue wrappedGet(int index) /*-{
-    return this.@com.google.gwt.json.client.JSONArray::wrappedArray[index];
-  }-*/;
-
-  private native void wrappedSet(int index, JSONValue jsonValue) /*-{
-    this.@com.google.gwt.json.client.JSONArray::wrappedArray[index] = jsonValue;
-  }-*/;
-
-  private native boolean wrappedTest(int index) /*-{
-    var x = this.@com.google.gwt.json.client.JSONArray::wrappedArray[index];
-    return x !== undefined;
+    this.@com.google.gwt.json.client.JSONArray::jsArray[index] = value;
   }-*/;
 }
diff --git a/user/src/com/google/gwt/json/client/JSONBoolean.java b/user/src/com/google/gwt/json/client/JSONBoolean.java
index c4ba714..d92b8f1 100644
--- a/user/src/com/google/gwt/json/client/JSONBoolean.java
+++ b/user/src/com/google/gwt/json/client/JSONBoolean.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.json.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+
 /**
  * Represents a JSON boolean value.
  */
@@ -40,6 +42,14 @@
     }
   }
 
+  /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static boolean unwrap(JSONBoolean value) {
+    return value.value;
+  }
+
   private final boolean value;
 
   /*
@@ -72,4 +82,9 @@
   public String toString() {
     return Boolean.toString(value);
   }
+
+  @Override
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONBoolean::unwrap(Lcom/google/gwt/json/client/JSONBoolean;);
+  }-*/;
 }
diff --git a/user/src/com/google/gwt/json/client/JSONNull.java b/user/src/com/google/gwt/json/client/JSONNull.java
index 7717099..9873f8a 100644
--- a/user/src/com/google/gwt/json/client/JSONNull.java
+++ b/user/src/com/google/gwt/json/client/JSONNull.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.json.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+
 /**
  * Represents the JSON <code>null</code> value.
  */
@@ -30,6 +32,14 @@
   }
 
   /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static JavaScriptObject unwrap() {
+    return null;
+  }
+
+  /**
    * There should only be one null value.
    */
   private JSONNull() {
@@ -51,4 +61,8 @@
     return "null";
   }
 
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONNull::unwrap();
+  }-*/;
+
 }
diff --git a/user/src/com/google/gwt/json/client/JSONNumber.java b/user/src/com/google/gwt/json/client/JSONNumber.java
index 6cb3640..2bd6682 100644
--- a/user/src/com/google/gwt/json/client/JSONNumber.java
+++ b/user/src/com/google/gwt/json/client/JSONNumber.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -15,11 +15,21 @@
  */
 package com.google.gwt.json.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+
 /**
  * Represents a JSON number. Numbers are represented by <code>double</code>s.
  */
 public class JSONNumber extends JSONValue {
 
+  /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static double unwrap(JSONNumber value) {
+    return value.value;
+  }
+
   private double value;
 
   /**
@@ -30,12 +40,36 @@
   }
 
   /**
-   * Gets the double value that this JSONNumber represents.
+   * Gets the double value this JSONNumber represents.
    */
+  public double doubleValue() {
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof JSONNumber)) {
+      return false;
+    }
+    return value == ((JSONNumber) other).value;
+  }
+
+  /**
+   * Gets the double value this JSONNumber represents.
+   * 
+   * @deprecated use {@link #doubleValue()}
+   */
+  @Deprecated
   public double getValue() {
     return value;
   }
 
+  @Override
+  public int hashCode() {
+    // Just use the underlying double's hashCode.
+    return Double.valueOf(value).hashCode();
+  }
+
   /**
    * Returns <code>this</code>, as this is a JSONNumber.
    */
@@ -52,4 +86,9 @@
     // Use JavaScript conversion so that integral values print as integers.
     return this.@com.google.gwt.json.client.JSONNumber::value + "";
   }-*/;
+
+  @Override
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONNumber::unwrap(Lcom/google/gwt/json/client/JSONNumber;);
+  }-*/;
 }
diff --git a/user/src/com/google/gwt/json/client/JSONObject.java b/user/src/com/google/gwt/json/client/JSONObject.java
index 84043e7..e94b0b7 100644
--- a/user/src/com/google/gwt/json/client/JSONObject.java
+++ b/user/src/com/google/gwt/json/client/JSONObject.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -17,102 +17,85 @@
 
 import com.google.gwt.core.client.JavaScriptObject;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 /**
- * Represents a JSON object. A JSON object is a map of string-based keys onto a
- * set of {@link com.google.gwt.json.client.JSONValue} objects.
+ * Represents a JSON object. A JSON object consists of a set of properties.
  */
 public class JSONObject extends JSONValue {
 
-  private static native void addAllKeysFromJavascriptObject(Set<String> s,
-      JavaScriptObject javaScriptObject) /*-{
-    for(var key in javaScriptObject) {
-      s.@java.util.Set::add(Ljava/lang/Object;)(key);
-    }
-  }-*/;
+  /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static JavaScriptObject unwrap(JSONObject value) {
+    return value.jsObject;
+  }
 
-  private static native boolean containsBack(JavaScriptObject backStore, String key) /*-{
-    key = String(key);
-    return Object.prototype.hasOwnProperty.call(backStore, key);
-  }-*/;
-
-  private static native JSONValue getFront(JavaScriptObject frontStore, String key) /*-{
-    key = String(key);
-    return Object.prototype.hasOwnProperty.call(frontStore, key) ? frontStore[key] : null;
-  }-*/;
-
-  private static native void putFront(JavaScriptObject frontStore, String key,
-      JSONValue jsonValue) /*-{
-    frontStore[String(key)] = jsonValue;
-  }-*/;
-
-  private static native Object removeBack(JavaScriptObject backStore, String key) /*-{
-    key = String(key);
-    var result = backStore[key];
-    delete backStore[key];
-    if (typeof result != 'object') {
-      result = Object(result); 
-    }
-    return result;
-  }-*/;
-
-  private final JavaScriptObject backStore;
-
-  private final JavaScriptObject frontStore = JavaScriptObject.createObject();
+  private final JavaScriptObject jsObject;
 
   public JSONObject() {
-    backStore = JavaScriptObject.createObject();
+    jsObject = JavaScriptObject.createObject();
   }
 
   /**
    * Creates a new JSONObject from the supplied JavaScript value.
    */
   public JSONObject(JavaScriptObject jsValue) {
-    backStore = jsValue;
+    jsObject = jsValue;
   }
 
   /**
-   * Tests whether or not this JSONObject contains the specified key.
+   * Tests whether or not this JSONObject contains the specified property.
    * 
-   * We use Object.hasOwnProperty here to verify that a given key is specified
-   * on this object rather than a superclass (such as standard properties
-   * defined on Object).
-   * 
-   * @param key the key to search for
-   * @return <code>true</code> if the JSONObject contains the specified key
+   * @param key the property to search for
+   * @return <code>true</code> if the JSONObject contains the specified property
    */
-  public boolean containsKey(String key) {
-    return get(key) != null;
+  public native boolean containsKey(String key) /*-{
+    return this.@com.google.gwt.json.client.JSONObject::jsObject[key] !== undefined;
+  }-*/;
+
+  /**
+   * Returns <code>true</code> if <code>other</code> is a {@link JSONObject}
+   * wrapping the same underlying object.
+   */
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof JSONObject)) {
+      return false;
+    }
+    return jsObject.equals(((JSONObject) other).jsObject);
   }
 
   /**
-   * Gets the JSONValue associated with the specified key.
+   * Gets the JSONValue associated with the specified property.
    * 
-   * We use Object.hasOwnProperty here to verify that a given key is specified
-   * on this object rather than a superclass (such as standard properties
-   * defined on Object).
-   * 
-   * @param key the key to search for
-   * @return if found, the value associated with the specified key, or
-   *         <code>null</code> otherwise
+   * @param key the property to access
+   * @return the value of the specified property, or <code>null</code> if the
+   *         property does not exist
+   * @throws NullPointerException if key is <code>null</code>
    */
   public JSONValue get(String key) {
     if (key == null) {
-      return null;
+      throw new NullPointerException();
     }
-    JSONValue result = getFront(frontStore, key);
-    if (result == null && containsBack(backStore, key)) {
-      Object o = removeBack(backStore, key);
-      if (o instanceof String) {
-        result = new JSONString((String) o);
-      } else {
-        result = JSONParser.buildValue((JavaScriptObject) o);
-      }
-      putFront(frontStore, key, result);
-    }
-    return result;
+    return get0(key);
+  }
+
+  /**
+   * Returns the underlying JavaScript object that this object wraps.
+   */
+  public JavaScriptObject getJavaScriptObject() {
+    return jsObject;
+  }
+  
+  @Override
+  public int hashCode() {
+    return jsObject.hashCode();
   }
 
   /**
@@ -124,25 +107,22 @@
   }
 
   /**
-   * Returns keys for which this JSONObject has associations.
-   * 
-   * @return array of keys for which there is a value
+   * Returns the set of properties defined on this JSONObject.
    */
   public Set<String> keySet() {
-    Set<String> keySet = new HashSet<String>();
-    addAllKeysFromJavascriptObject(keySet, frontStore);
-    addAllKeysFromJavascriptObject(keySet, backStore);
+    HashSet<String> keySet = new HashSet<String>();
+    addAllKeys(keySet);
     return keySet;
   }
 
   /**
-   * Maps the specified key to the specified value in this JSONObject. If the
-   * specified key already has an associated value, it is overwritten.
+   * Assign the specified property to the specified value in this JSONObject. If
+   * the property already has an associated value, it is overwritten.
    * 
-   * @param key the key to associate with the specified value
-   * @param jsonValue the value to associate with this key
-   * @return if one existed, the previous value associated with the key, or
-   *         <code>null</code> otherwise
+   * @param key the property to assign
+   * @param jsonValue the value to assign
+   * @return the previous value of the property, or <code>null</code> if the
+   *         property did not exist
    * @throws NullPointerException if key is <code>null</code>
    */
   public JSONValue put(String key, JSONValue jsonValue) {
@@ -150,12 +130,12 @@
       throw new NullPointerException();
     }
     JSONValue previous = get(key);
-    putFront(frontStore, key, jsonValue);
+    put0(key, jsonValue);
     return previous;
   }
 
   /**
-   * Determines the number of keys on this object.
+   * Determines the number of properties on this object.
    */
   public int size() {
     return keySet().size();
@@ -168,28 +148,53 @@
    * @return a JSON string representation of this JSONObject instance
    */
   @Override
-  public native String toString() /*-{
-    for (var key in this.@com.google.gwt.json.client.JSONObject::backStore) {
-      // Wrap everything in backStore so that frontStore is canonical.
-      this.@com.google.gwt.json.client.JSONObject::get(Ljava/lang/String;)(key);
-    }
-    var out = [];
-    out.push("{");
-    var first = true;
-    for (var key in this.@com.google.gwt.json.client.JSONObject::frontStore) {
-      if(first) {
+  public String toString() {
+    StringBuffer sb = new StringBuffer();
+    sb.append("{");
+    boolean first = true;
+    List<String> keys = new ArrayList<String>();
+    addAllKeys(keys);
+    for (String key : keys) {
+      if (first) {
         first = false;
       } else {
-        out.push(", ");
+        sb.append(", ");
       }
-      var subObj = 
-        (this.@com.google.gwt.json.client.JSONObject::frontStore[key]).
-          @com.google.gwt.json.client.JSONValue::toString()();
-      out.push(@com.google.gwt.json.client.JSONString::escapeValue(Ljava/lang/String;)(key));
-      out.push(":");
-      out.push(subObj);
+      sb.append(JSONString.escapeValue(key));
+      sb.append(":");
+      sb.append(get(key));
     }
-    out.push("}")
-    return out.join("");
+    sb.append("}");
+    return sb.toString();
+  }
+
+  @Override
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONObject::unwrap(Lcom/google/gwt/json/client/JSONObject;);
+  }-*/;
+
+  private native void addAllKeys(Collection<String> s) /*-{
+    var jsObject = this.@com.google.gwt.json.client.JSONObject::jsObject;
+    for (var key in jsObject) {
+      if (jsObject[key] !== undefined) {
+        s.@java.util.Collection::add(Ljava/lang/Object;)(key);
+      }
+    }
+  }-*/;
+
+  private native JSONValue get0(String key) /*-{
+    var v = this.@com.google.gwt.json.client.JSONObject::jsObject[key];
+    var func = @com.google.gwt.json.client.JSONParser::typeMap[typeof v];
+    return func ? func(v) : @com.google.gwt.json.client.JSONParser::throwUnknownTypeException(Ljava/lang/String;)(typeof v);
+  }-*/;
+
+  private native void put0(String key, JSONValue value) /*-{
+    if (value === null) {
+      value = undefined;
+    } else {
+      var func = value.@com.google.gwt.json.client.JSONValue::getUnwrapper()();
+      value = func(value);
+    }
+    this.@com.google.gwt.json.client.JSONObject::jsObject[key] = value;
   }-*/;
 }
diff --git a/user/src/com/google/gwt/json/client/JSONParser.java b/user/src/com/google/gwt/json/client/JSONParser.java
index c4e4157..89c9828 100644
--- a/user/src/com/google/gwt/json/client/JSONParser.java
+++ b/user/src/com/google/gwt/json/client/JSONParser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2008 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
@@ -26,19 +26,21 @@
  */
 public class JSONParser {
 
+  static final JavaScriptObject typeMap = initTypeMap();
+
   /**
-   * Given a jsonString, returns the JSONObject representation. For efficiency,
-   * parsing occurs lazily as the structure is requested.
+   * Evaluates a trusted JSON string and returns its JSONValue representation.
+   * CAUTION! For efficiency, this method is implemented using the JavaScript
+   * <code>eval()</code> function, which can execute arbitrary script. DO NOT
+   * pass an untrusted string into this method.
    * 
-   * @param jsonString
-   * @return a JSONObject that has been built by parsing the JSON string
+   * @param jsonString a JSON object to parse
+   * @return a JSONValue that has been built by parsing the JSON string
    * @throws NullPointerException if <code>jsonString</code> is
    *           <code>null</code>
    * @throws IllegalArgumentException if <code>jsonString</code> is empty
    */
   public static JSONValue parse(String jsonString) {
-    // Create a JavaScriptObject from the JSON string.
-    //
     if (jsonString == null) {
       throw new NullPointerException();
     }
@@ -46,142 +48,96 @@
       throw new IllegalArgumentException("empty argument");
     }
     try {
-      Object object = evaluate(jsonString);
-      if (object instanceof String) {
-        return new JSONString((String) object);
-      } else {
-        return buildValue((JavaScriptObject) object);
-      }
+      return evaluate(jsonString);
     } catch (JavaScriptException ex) {
       throw new JSONException(ex);
     }
   }
 
-  /**
-   * Returns the {@link JSONValue} for a given {@link JavaScriptObject}.  
-   * 
-   * @param jsValue {@link JavaScriptObject} to build a {@link JSONValue} for, 
-   *     this object cannot be a primitive JavaScript type
-   * @return a {@link JSONValue} instance for the {@link JavaScriptObject}
-   */
-  static JSONValue buildValue(JavaScriptObject jsValue) throws JSONException {
-    
-    if (isNull(jsValue)) {
-      return JSONNull.getInstance();
-    }
-
-    if (isArray(jsValue)) {
-      return new JSONArray(jsValue);
-    }
-
-    if (isBoolean(jsValue)) {
-      return JSONBoolean.getInstance(asBoolean(jsValue));
-    }
-
-    if (isDouble(jsValue)) {
-      return new JSONNumber(asDouble(jsValue));
-    }
-
-    if (isObject(jsValue)) {
-      return new JSONObject(jsValue);
-    }
-
-    /*
-     * In practice we should never reach this point.  If we do, we cannot make 
-     * any assumptions about the jsValue.
-     */
-    throw new JSONException("Unknown JavaScriptObject type");
+  static void throwUnknownTypeException(String typeString) {
+    throw new JSONException("Unexpected typeof result '" + typeString
+        + "'; please report this bug to the GWT team");
   }
 
   /**
-   * Returns the boolean represented by the jsValue. This method
-   * assumes that {@link #isBoolean(JavaScriptObject)} returned
-   * <code>true</code>.
-   * 
-   * @param jsValue JavaScript object to convert
-   * @return the boolean represented by the jsValue
+   * Called from {@link #initTypeMap()}.
    */
-  private static native boolean asBoolean(JavaScriptObject jsValue) /*-{
-    return jsValue.valueOf();
-  }-*/;
+  @SuppressWarnings("unused")
+  private static JSONValue createBoolean(boolean v) {
+    return JSONBoolean.getInstance(v);
+  }
 
   /**
-   * Returns the double represented by jsValue. This method assumes that
-   * {@link #isDouble(JavaScriptObject)} returned <code>true</code>.
-   * 
-   * @param jsValue JavaScript object to convert
-   * @return the double represented by the jsValue
+   * Called from {@link #initTypeMap()}.
    */
-  private static native double asDouble(JavaScriptObject jsValue) /*-{
-    return jsValue.valueOf();
-  }-*/;
+  @SuppressWarnings("unused")
+  private static JSONValue createNumber(double v) {
+    return new JSONNumber(v);
+  }
 
   /**
-   * This method converts the json string into either a String or a
-   * JavaScriptObject by simply evaluating the string in JavaScript.
+   * Called from {@link #initTypeMap()}. If we get here, <code>o</code> is
+   * either <code>null</code> (not <code>undefined</code>) or a JavaScript
+   * object.
    */
-  private static native Object evaluate(String jsonString) /*-{
-    var x = eval('(' + jsonString + ')');
-    if (typeof x == 'number' || typeof x == 'array' || typeof x == 'boolean') {
-      x = (Object(x));
+  @SuppressWarnings("unused")
+  private static native JSONValue createObject(Object o) /*-{
+    if (!o) {
+      return @com.google.gwt.json.client.JSONNull::getInstance()();
     }
-    return x;
+    var v = o.valueOf ? o.valueOf() : o;
+    if (v !== o) {
+      // It was a primitive wrapper, unwrap it and try again.
+      var func = @com.google.gwt.json.client.JSONParser::typeMap[typeof v];
+      return func ? func(v) : @com.google.gwt.json.client.JSONParser::throwUnknownTypeException(Ljava/lang/String;)(typeof v);
+    } else if (o instanceof Array || o instanceof $wnd.Array) {
+      // Looks like an Array; wrap as JSONArray.
+      // NOTE: this test can fail for objects coming from a different window,
+      // but we know of no reliable tests to determine if something is an Array
+      // in all cases.
+      return @com.google.gwt.json.client.JSONArray::new(Lcom/google/gwt/core/client/JavaScriptObject;)(o);
+    } else {
+      // This is a basic JavaScript object; wrap as JSONObject.
+      // Subobjects will be created on demand.
+      return @com.google.gwt.json.client.JSONObject::new(Lcom/google/gwt/core/client/JavaScriptObject;)(o);
+    }
   }-*/;
 
   /**
-   * Returns <code>true</code> if the {@link JavaScriptObject} is a wrapped
-   * JavaScript Array.
-   * 
-   * @param jsValue JavaScript object to test
-   * @return <code>true</code> if jsValue is a wrapped JavaScript Array
+   * Called from {@link #initTypeMap()}.
    */
-  private static native boolean isArray(JavaScriptObject jsValue) /*-{
-    return jsValue instanceof Array; 
-  }-*/;
+  @SuppressWarnings("unused")
+  private static JSONValue createString(String v) {
+    return new JSONString(v);
+  }
 
   /**
-   * Returns <code>true</code> if the {@link JavaScriptObject} is a wrapped
-   * JavaScript Boolean.
-   * 
-   * @param jsValue JavaScript object to test
-   * @return <code>true</code> if jsValue is a wrapped JavaScript Boolean
+   * Called from {@link #initTypeMap()}. This method returns a
+   * <code>null</code> pointer, representing JavaScript <code>undefined</code>.
    */
-  private static native boolean isBoolean(JavaScriptObject jsValue) /*-{
-    return jsValue instanceof Boolean; 
-  }-*/;
+  @SuppressWarnings("unused")
+  private static JSONValue createUndefined() {
+    return null;
+  }
 
   /**
-   * Returns <code>true</code> if the {@link JavaScriptObject} is a wrapped
-   * JavaScript Double.
-   * 
-   * @param jsValue JavaScript object to test
-   * @return <code>true</code> if jsValue is a wrapped JavaScript Double
+   * This method converts <code>jsonString</code> into a JSONValue.
    */
-  private static native boolean isDouble(JavaScriptObject jsValue) /*-{
-    return jsValue instanceof Number;
+  private static native JSONValue evaluate(String jsonString) /*-{
+    var v = eval('(' + jsonString + ')');
+    var func = @com.google.gwt.json.client.JSONParser::typeMap[typeof v];
+    return func ? func(v) : @com.google.gwt.json.client.JSONParser::throwUnknownTypeException(Ljava/lang/String;)(typeof v);
   }-*/;
 
-  /**
-   * Returns <code>true</code> if the {@link JavaScriptObject} is <code>null</code>
-   * or <code>undefined</code>.
-   * 
-   * @param jsValue JavaScript object to test
-   * @return <code>true</code> if jsValue is <code>null</code> or
-   *         <code>undefined</code>
-   */
-  private static native boolean isNull(JavaScriptObject jsValue) /*-{
-    return jsValue == null;
-  }-*/;
-
-  /**
-   * Returns <code>true</code> if the {@link JavaScriptObject} is a JavaScript
-   * Object.
-   * 
-   * @param jsValue JavaScript object to test
-   * @return <code>true</code> if jsValue is a JavaScript Object
-   */
-  private static native boolean isObject(JavaScriptObject jsValue) /*-{
-    return jsValue instanceof Object;
+  private static native JavaScriptObject initTypeMap() /*-{
+    return {
+      "boolean": @com.google.gwt.json.client.JSONParser::createBoolean(Z),
+      "number": @com.google.gwt.json.client.JSONParser::createNumber(D),
+      "string": @com.google.gwt.json.client.JSONParser::createString(Ljava/lang/String;),
+      "object": @com.google.gwt.json.client.JSONParser::createObject(Ljava/lang/Object;),
+      "function": @com.google.gwt.json.client.JSONParser::createObject(Ljava/lang/Object;),
+      "undefined": @com.google.gwt.json.client.JSONParser::createUndefined(),
+    }
   }-*/;
 
   /**
diff --git a/user/src/com/google/gwt/json/client/JSONString.java b/user/src/com/google/gwt/json/client/JSONString.java
index 73bce23..5ad02d0 100644
--- a/user/src/com/google/gwt/json/client/JSONString.java
+++ b/user/src/com/google/gwt/json/client/JSONString.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -49,6 +49,14 @@
     return out;
   }-*/;
 
+  /**
+   * Called from {@link #getUnwrapper()}. 
+   */
+  @SuppressWarnings("unused")
+  private static String unwrap(JSONString value) {
+    return value.value;
+  }
+
   private String value;
 
   /**
@@ -64,6 +72,20 @@
     this.value = value;
   }
 
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof JSONString)) {
+      return false;
+    }
+    return value.equals(((JSONString) other).value);
+  }
+
+  @Override
+  public int hashCode() {
+    // Just use the underlying String's hashCode.
+    return value.hashCode();
+  }
+
   /**
    * Returns <code>this</code>, as this is a JSONString.
    */
@@ -87,4 +109,9 @@
   public String toString() {
     return escapeValue(value);
   }
+
+  @Override
+  native JavaScriptObject getUnwrapper() /*-{
+    return @com.google.gwt.json.client.JSONString::unwrap(Lcom/google/gwt/json/client/JSONString;);
+  }-*/;
 }
diff --git a/user/src/com/google/gwt/json/client/JSONValue.java b/user/src/com/google/gwt/json/client/JSONValue.java
index 4650109..7c2afae 100644
--- a/user/src/com/google/gwt/json/client/JSONValue.java
+++ b/user/src/com/google/gwt/json/client/JSONValue.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.json.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+
 /**
  * The superclass of all JSON value types.
  * 
@@ -26,6 +28,12 @@
  */
 public abstract class JSONValue {
   /**
+   * Not subclassable outside this package.
+   */
+  JSONValue() {
+  }
+
+  /**
    * Returns a non-null reference if this JSONValue is really a JSONArray.
    * 
    * @return a reference to a JSONArray if this JSONValue is a JSONArray or
@@ -90,5 +98,11 @@
    * JSON strings that can be sent from the client to a server.
    */
   @Override
-  public abstract String toString() throws JSONException;
+  public abstract String toString();
+
+  /**
+   * Internal. Returns a JS func that can unwrap this value.  Used from native
+   * code.
+   */
+  abstract JavaScriptObject getUnwrapper();
 }
diff --git a/user/test/com/google/gwt/json/client/JSONTest.java b/user/test/com/google/gwt/json/client/JSONTest.java
index 3617e27..de9f9f0 100644
--- a/user/test/com/google/gwt/json/client/JSONTest.java
+++ b/user/test/com/google/gwt/json/client/JSONTest.java
@@ -79,7 +79,7 @@
     } else if (expected.isNumber() != null) {
       JSONNumber expNum = expected.isNumber();
       JSONNumber actNum = actual.isNumber();
-      assertEquals(expNum.getValue(), actNum.getValue());
+      assertEquals(expNum.doubleValue(), actNum.doubleValue());
     } else if (expected.isObject() != null) {
       JSONObject expObj = expected.isObject();
       JSONObject actObj = actual.isObject();
@@ -146,7 +146,7 @@
     assertEquals("Array size must be 10", 10, array.size());
     for (int i = 0; i < 10; i++) {
       assertEquals("Array value at " + i + " must be " + i,
-          array.get(i).isNumber().getValue(), i, 0.001);
+          array.get(i).isNumber().doubleValue(), i, 0.001);
     }
   }
 
@@ -163,6 +163,45 @@
     assertFalse(falseVal.isBoolean().booleanValue());
   }
 
+  public void testEquals() {
+    JSONArray array = JSONParser.parse("[]").isArray();
+    assertEquals(array, new JSONArray(array.getJavaScriptObject()));
+
+    assertEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
+    assertEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));
+
+    assertEquals(JSONNull.getInstance(), JSONNull.getInstance());
+
+    assertEquals(new JSONNumber(3.1), new JSONNumber(3.1));
+
+    JSONObject object = JSONParser.parse("{}").isObject();
+    assertEquals(object, new JSONObject(object.getJavaScriptObject()));
+
+    assertEquals(new JSONString("foo"), new JSONString("foo"));
+  }
+  
+  public void testHashCode() {
+    JSONArray array = JSONParser.parse("[]").isArray();
+    assertHashCodeEquals(array, new JSONArray(array.getJavaScriptObject()));
+
+    assertHashCodeEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
+    assertHashCodeEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));
+
+    assertHashCodeEquals(JSONNull.getInstance(), JSONNull.getInstance());
+
+    assertHashCodeEquals(new JSONNumber(3.1), new JSONNumber(3.1));
+
+    JSONObject object = JSONParser.parse("{}").isObject();
+    assertHashCodeEquals(object, new JSONObject(object.getJavaScriptObject()));
+
+    assertHashCodeEquals(new JSONString("foo"), new JSONString("foo"));
+  }
+
+  private void assertHashCodeEquals(Object expected, Object actual) {
+    assertEquals("hashCodes are not equal", expected.hashCode(),
+        actual.hashCode());
+  }
+
   // Null characters do not work in hosted mode
   public void testEscaping() {
     JSONObject o = new JSONObject();
@@ -258,17 +297,17 @@
 
   public void testNumberBasics() {
     JSONNumber n0 = new JSONNumber(1000);
-    assertEquals(1000, n0.getValue(), .000001);
+    assertEquals(1000, n0.doubleValue(), .000001);
     assertTrue(n0.isNumber() == n0);
     assertNull(n0.isObject());
 
     JSONNumber n1 = new JSONNumber(Integer.MAX_VALUE);
-    assertEquals(Integer.MAX_VALUE, n1.getValue(), .00001);
+    assertEquals(Integer.MAX_VALUE, n1.doubleValue(), .00001);
     assertTrue(n1.isNumber() == n1);
     assertNull(n1.isObject());
 
     JSONNumber n2 = new JSONNumber(Integer.MIN_VALUE);
-    assertEquals(Integer.MIN_VALUE, n2.getValue(), .00001);
+    assertEquals(Integer.MIN_VALUE, n2.doubleValue(), .00001);
     assertTrue(n2.isNumber() == n2);
     assertNull(n2.isObject());
   }
@@ -296,7 +335,7 @@
     assertEquals("Object size must be 10", 10, objIn.keySet().size());
     for (int i = 0; i < 10; i++) {
       assertEquals("Object value at 'Object " + i + "' must be " + i,
-          objIn.get("Object " + i).isNumber().getValue(), i, 0.001);
+          objIn.get("Object " + i).isNumber().doubleValue(), i, 0.001);
     }
   }
 
@@ -327,7 +366,7 @@
     assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
         JSONParser.parse("null"));
     assertEquals("5 should be JSONNumber 5", 5d,
-        JSONParser.parse("5").isNumber().getValue(), 0.001);
+        JSONParser.parse("5").isNumber().doubleValue(), 0.001);
     assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
         JSONParser.parse("null"));
     JSONValue somethingHello = JSONParser.parse("[{\"something\":\"hello\"}]");
@@ -402,7 +441,34 @@
         object.get(stringAsPrimitive("null")).isString().stringValue());
     assertEquals("foo",
         object.get(stringAsObject("null")).isString().stringValue());
-    assertNull(object.get(null));
+
+    try {
+      assertNull(object.get(null));
+      fail("Expected NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testUndefined() {
+    JSONObject o = JSONParser.parse("{foo:'foo',bar:null}").isObject();
+    assertEquals(new JSONString("foo"), o.get("foo"));
+    assertEquals(JSONNull.getInstance(), o.get("bar"));
+    assertNull(o.get("baz"));
+
+    o.put("foo", JSONNull.getInstance());
+    assertEquals(JSONNull.getInstance(), o.get("foo"));
+    o.put("foo", null);
+    assertNull(o.get("foo"));
+
+    JSONArray array = JSONParser.parse("['foo',null]").isArray();
+    assertEquals(new JSONString("foo"), array.get(0));
+    assertEquals(JSONNull.getInstance(), array.get(1));
+    assertNull(array.get(2));
+
+    array.set(0, JSONNull.getInstance());
+    assertEquals(JSONNull.getInstance(), array.get(0));
+    array.set(0, null);
+    assertNull(array.get(0));
   }
 
   public void testWidget() {