TAP5-2640: tapestry-json improvements

(implement Collection/Map, better exceptions)

tapestry-json: added JSONTypeMismatchException

Added JSONTypeMismatchException and JSONValueNotFoundException. JSONObject behaviour improved by using opt() isntead of get for mor specific types, so we can actually use the new exceptions.

tapestry-json: JSONArray get() now throws IndexOutOfBoundsException

There's no reason to repackage the original exception into a RuntimeException.

tapestry-json: JSONTokener constructors exceptions improved

tapestry-json: improve JSONSpec tests

tapestry-json: better exception building

tapestry-json: added JSONSyntaxException

tapesty-json: javadoc updated

tapestry-json: JSONArray non-finite/nan check fixed

The constructor wasn't using checkedPut, even though the javadoc states that doubles are checked.

tapestry-json: javadoc typo

tapestry-json: added JSONArrayIndexOutOfBoundsException

tapestry-json: code style

tapestrsy-json: fix lossy conversion

tapestry-json: source formatting

tapestry-json: throw IllegalArgumentException on invalid doubles

tapestry-json: JSONObject implements Map<String, Object>

To provide better interoperability with Java collections the JSONObject type now implements Map<String, Object>.

The needed changes are marginal:
- putAll -> now returns void (breaking change)
- Arguments for key are Object -> shouldn't be a problem

tapestry-json: added get{type}OrDefault methods to JSONObject

tapestry-json: make JSONArray a "real" Collection (wip)

tapestry-json: JSONArray improve javadoc

tapestry-json: JSONObject improve javadoc
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
index 955ee44..169de73 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSON.java
@@ -16,13 +16,15 @@
 
 package org.apache.tapestry5.json;
 
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException;
+
 class JSON {
     /**
      * Returns the input if it is a JSON-permissible value; throws otherwise.
      */
-    static double checkDouble(double d) throws RuntimeException {
+    static double checkDouble(double d) throws IllegalArgumentException {
         if (Double.isInfinite(d) || Double.isNaN(d)) {
-            throw new RuntimeException("JSON does not allow non-finite numbers.");
+            throw new IllegalArgumentException("JSON does not allow non-finite numbers.");
         }
         return d;
     }
@@ -92,24 +94,29 @@
         return null;
     }
 
-    static RuntimeException typeMismatch(boolean array, Object indexOrName, Object actual,
-            String requiredType) throws RuntimeException {
-        String location = array ? "JSONArray[" + indexOrName + "]" : "JSONObject[\"" + indexOrName + "\"]";
-        if (actual == null) {
-            throw new RuntimeException(location + " is null.");
-        } else {
-            throw new RuntimeException(location + " is not a " + requiredType + ".");
+    static void testValidity(Object value)
+    {
+        if (value == null) {
+            throw new IllegalArgumentException("null isn't valid in JSONArray. Use JSONObject.NULL instead.");
         }
-    }
 
-    static RuntimeException typeMismatch(Object actual, String requiredType)
-            throws RuntimeException {
-        if (actual == null) {
-            throw new RuntimeException("Value is null.");
-        } else {
-            throw new RuntimeException("Value " + actual
-                    + " of type " + actual.getClass().getName()
-                    + " cannot be converted to " + requiredType);
+        if (value == JSONObject.NULL)
+        {
+            return;
         }
+
+        Class<? extends Object> clazz = value.getClass();
+        if (Boolean.class.isAssignableFrom(clazz)
+            || Number.class.isAssignableFrom(clazz)
+            || String.class.isAssignableFrom(clazz)
+            || JSONArray.class.isAssignableFrom(clazz)
+            || JSONLiteral.class.isAssignableFrom(clazz)
+            || JSONObject.class.isAssignableFrom(clazz)
+            || JSONString.class.isAssignableFrom(clazz))
+        {
+            return;
+        }
+
+        throw new JSONInvalidTypeException(clazz);
     }
 }
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
index c1fb7f4..f4fcc2f 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONArray.java
@@ -17,10 +17,16 @@
 package org.apache.tapestry5.json;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
+import org.apache.tapestry5.json.exceptions.JSONArrayIndexOutOfBoundsException;
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException;
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
 // Note: this class was written without inspecting the non-free org.json sourcecode.
 
 /**
@@ -41,7 +47,7 @@
  *
  * Instances of this class are not thread safe.
  */
-public final class JSONArray extends JSONCollection implements Iterable<Object> {
+public final class JSONArray extends JSONCollection implements Collection<Object> {
 
     private final List<Object> values;
 
@@ -58,7 +64,8 @@
      *
      * @param readFrom a tokener whose nextValue() method will yield a
      *                 {@code JSONArray}.
-     * @throws RuntimeException if the parse fails or doesn't yield a
+     * @throws JSONSyntaxException if the parse fails
+     * @throws JSONTypeMismatchException if it doesn't yield a
      *                       {@code JSONArray}.
      */
     JSONArray(JSONTokener readFrom) {
@@ -70,7 +77,7 @@
         if (object instanceof JSONArray) {
             values = ((JSONArray) object).values;
         } else {
-            throw JSON.typeMismatch(object, "JSONArray");
+            throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.ARRAY);
         }
     }
 
@@ -78,8 +85,9 @@
      * Creates a new {@code JSONArray} with values from the JSON string.
      *
      * @param json a JSON-encoded string containing an array.
-     * @throws RuntimeException if the parse fails or doesn't yield a {@code
-     *                       JSONArray}.
+     * @throws JSONSyntaxException if the parse fails
+     * @throws JSONTypeMismatchException if it doesn't yield a
+     *                       {@code JSONArray}.
      */
     public JSONArray(String json) {
         this(new JSONTokener(json));
@@ -89,17 +97,17 @@
      * Creates a new {@code JSONArray} with values from the given primitive array.
      *
      * @param values The values to use.
-     * @throws RuntimeException if any of the values are non-finite double values (i.e. NaN or infinite)
+     * @throws IllegalArgumentException if any of the values are non-finite double values (i.e. NaN or infinite)
      */
     public JSONArray(Object... values) {
         this();
         for (int i = 0; i < values.length; ++i) {
-            put(values[i]);
+            checkedPut(values[i]);
         }
     }
 
     /**
-     * Create a new array, and adds all values fro the iterable to the array (using {@link #putAll(Iterable)}.
+     * Create a new array, and adds all values from the iterable to the array (using {@link #putAll(Iterable)}.
      *
      * This is implemented as a static method so as not to break the semantics of the existing {@link #JSONArray(Object...)} constructor.
      * Adding a constructor of type Iterable would change the meaning of <code>new JSONArray(new JSONArray())</code>.
@@ -115,12 +123,39 @@
 
     /**
      * @return Returns the number of values in this array.
+     * @deprecated Use {@link #size()} instead.
      */
     public int length() {
+        return size();
+    }
+
+    /**
+     * Returns the number of values in this array.
+     * If this list contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of values in this array
+     * @since 5.7
+     */
+    @Override
+    public int size()
+    {
         return values.size();
     }
 
     /**
+     * Returns {@code true} if this array contains no values.
+     *
+     * @return {@code true} if this array contains no values
+     * @since 5.7
+     */
+    @Override
+    public boolean isEmpty()
+    {
+        return values.isEmpty();
+    }
+
+    /**
      * Appends {@code value} to the end of this array.
      *
      * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
@@ -129,20 +164,38 @@
      *              infinities}. Unsupported values are not permitted and will cause the
      *              array to be in an inconsistent state.
      * @return this array.
+     * @deprecated The use of {@link #add(Object)] is encouraged.
      */
     public JSONArray put(Object value) {
-        JSONObject.testValidity(value);
-        values.add(value);
+        add(value);
         return this;
     }
 
     /**
+     * Appends {@code value} to the end of this array.
+     *
+     * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
+     *              Integer, Long, Double, or {@link JSONObject#NULL}}. May
+     *              not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
+     *              infinities}. Unsupported values are not permitted and will cause the
+     *              array to be in an inconsistent state.
+     * @return {@code true} (as specified by {@link Collection#add})
+     * @since 5.7
+     */
+    @Override
+    public boolean add(Object value)
+    {
+        JSON.testValidity(value);
+        return values.add(value);
+    }
+
+    /**
      * Same as {@link #put}, with added validity checks.
      *
      * @param value The value to append.
      */
     void checkedPut(Object value) {
-        JSONObject.testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             JSON.checkDouble(((Number) value).doubleValue());
         }
@@ -161,14 +214,15 @@
      *              not be {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
      *              infinities}.
      * @return this array.
-     * @throws RuntimeException If the value cannot be represented as a finite double value.
+     * @throws IllegalArgumentException If the value cannot be represented as a finite double value.
+     * @throws ArrayIndexOutOfBoundsException if the index is lower than 0
      */
     public JSONArray put(int index, Object value) {
         if (index < 0)
         {
-            throw new RuntimeException("JSONArray[" + index + "] not found.");
+            throw new JSONArrayIndexOutOfBoundsException(index);
         }
-        JSONObject.testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             // deviate from the original by checking all Numbers, not just floats & doubles
             JSON.checkDouble(((Number) value).doubleValue());
@@ -197,7 +251,8 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if this array has no value at {@code index}, or if
+     * @throws JSONArrayIndexOutOfBoundsException if the given index is out of bounds.
+     * @throws JSONValueNotFoundException if this array has no value at {@code index}, or if
      *                       that value is the {@code null} reference. This method returns
      *                       normally if the value is {@code JSONObject#NULL}.
      */
@@ -205,11 +260,12 @@
         try {
             Object value = values.get(index);
             if (value == null) {
-                throw new RuntimeException("Value at " + index + " is null.");
+                throw JSONExceptionBuilder.valueNotFound(true, index, JSONType.ANY);
             }
             return value;
-        } catch (IndexOutOfBoundsException e) {
-            throw new RuntimeException("Index " + index + " out of range [0.." + values.size() + ")");
+        }
+        catch (IndexOutOfBoundsException e) {
+            throw new JSONArrayIndexOutOfBoundsException(index);
         }
     }
 
@@ -228,19 +284,74 @@
     }
 
     /**
+     * Removes the first occurrence of the specified value from this JSONArray,
+     * if it is present.
+     *
+     * @param value value to be removed from this JSONArray, if present
+     * @return {@code true} if the element was removed
+     * @since 5.7
+     */
+    @Override
+    public boolean remove(Object value)
+    {
+        return values.remove(value);
+    }
+
+    /**
+     * Removes from this JSONArray all of its values that are contained in the
+     * specified collection.
+     *
+     * @param collection collection containing value to be removed from this JSONArray
+     * @return {@code true} if this JSONArray changed as a result of the call
+     * @throws NullPointerException if the specified collection is null.
+     * @see Collection#contains(Object)
+     * @since 5.7
+     */
+    @Override
+    public boolean removeAll(Collection<?> collection)
+    {
+        return values.removeAll(collection);
+    }
+
+    /**
+     * Removes all of the values from this JSONArray.
+     * 
+     * @since 5.7
+     */
+    @Override
+    public void clear()
+    {
+        values.clear();
+    }
+
+    /**
+     * Retains only the values in this JSONArray that are contained in the
+     * specified collection.
+     *
+     * @param collection collection containing elements to be retained in this list
+     * @return {@code true} if this list changed as a result of the call
+     * @since 5.7
+     */
+    @Override
+    public boolean retainAll(Collection<?> c)
+    {
+        return values.retainAll(c);
+    }
+
+    /**
      * Returns the value at {@code index} if it exists and is a boolean or can
      * be coerced to a boolean.
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a boolean.
      */
     public boolean getBoolean(int index) {
         Object object = get(index);
         Boolean result = JSON.toBoolean(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "Boolean");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.BOOLEAN);
         }
         return result;
     }
@@ -251,14 +362,14 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a double.
      */
     public double getDouble(int index) {
         Object object = get(index);
         Double result = JSON.toDouble(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "number");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -269,14 +380,14 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a int.
      */
     public int getInt(int index) {
         Object object = get(index);
         Integer result = JSON.toInteger(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "int");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -287,14 +398,14 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value at {@code index} doesn't exist or
+     * @throws JSONTypeMismatchException if the value at {@code index} doesn't exist or
      *                       cannot be coerced to a long.
      */
     public long getLong(int index) {
         Object object = get(index);
         Long result = JSON.toLong(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "long");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -305,13 +416,13 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if no such value exists.
+     * @throws JSONTypeMismatchException if no such value exists.
      */
     public String getString(int index) {
         Object object = get(index);
         String result = JSON.toString(object);
         if (result == null) {
-            throw JSON.typeMismatch(true, index, object, "String");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.STRING);
         }
         return result;
     }
@@ -322,7 +433,7 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value doesn't exist or is not a {@code
+     * @throws JSONTypeMismatchException if the value doesn't exist or is not a {@code
      *                       JSONArray}.
      */
     public JSONArray getJSONArray(int index) {
@@ -330,7 +441,7 @@
         if (object instanceof JSONArray) {
             return (JSONArray) object;
         } else {
-            throw JSON.typeMismatch(true, index, object, "JSONArray");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.ARRAY);
         }
     }
 
@@ -340,7 +451,7 @@
      *
      * @param index Which value to get.
      * @return the value at the specified location.
-     * @throws RuntimeException if the value doesn't exist or is not a {@code
+     * @throws JSONTypeMismatchException if the value doesn't exist or is not a {@code
      *                       JSONObject}.
      */
     public JSONObject getJSONObject(int index) {
@@ -348,7 +459,7 @@
         if (object instanceof JSONObject) {
             return (JSONObject) object;
         } else {
-            throw JSON.typeMismatch(true, index, object, "JSONObject");
+            throw JSONExceptionBuilder.typeMismatch(true, index, object, JSONType.OBJECT);
         }
     }
 
@@ -413,6 +524,30 @@
     }
 
     /**
+     * Adds all objects from the collection into this JSONArray, using {@link #add(Object)}.
+     *
+     * @param collection Any collection, or null
+     * @return boolean true, if JSONArray was changed.
+     * @since 5.7
+     */
+    @Override
+    public boolean addAll(Collection<? extends Object> collection)
+    {
+        if (collection == null)
+        { 
+            return false;
+        }
+
+        boolean changed = false;
+        for (Object value : collection)
+        {
+            changed = add(value) || changed;
+        }
+
+        return changed;
+    }
+
+    /**
      * Returns an unmodifiable list of the contents of the array. This is a wrapper around the list's internal
      * storage and is live (changes to the JSONArray affect the returned List).
      *
@@ -424,12 +559,84 @@
         return Collections.unmodifiableList(values);
     }
 
+    /**
+     * Returns an array containing all of the values in this JSONArray in proper
+     * sequence.
+     *
+     * @return an array containing all of the values in this JSONArray in proper
+     *         sequence
+     * @since 5.7
+     */
+    @Override
+    public Object[] toArray()
+    {
+        return values.toArray();
+    }
 
+    /**
+     * Returns an array containing all of the values in this JSONArray in
+     * proper sequence; the runtime type of the returned array is that of
+     * the specified array.
+     *
+     * @param array
+     *            the array into which the values of this JSONArray are to
+     *            be stored, if it is big enough; otherwise, a new array of the
+     *            same runtime type is allocated for this purpose.
+     * @return an array containing the values of this JSONArray
+     * @throws ArrayStoreException
+     *             if the runtime type of the specified array
+     *             is not a supertype of the runtime type of every element in
+     *             this list
+     * @throws NullPointerException
+     *             if the specified array is null
+     * @since 5.7
+     */
+    @Override
+    public <T> T[] toArray(T[] array)
+    {
+        return values.toArray(array);
+    }
+
+    /**
+     * Returns an iterator over the values in this array in proper sequence.
+     *
+     * @return an iterator over the values in this array in proper sequence
+     */
     @Override
     public Iterator<Object> iterator()
     {
         return values.iterator();
     }
 
+    /**
+     * Returns {@code true} if this JSONArray contains the specified value.
+     *
+     * @param value value whose presence in this JSONArray is to be tested
+     * @return {@code true} if this JSONArray contains the specified
+     *         value
+     * @since 5.7
+     */
+    @Override
+    public boolean contains(Object value)
+    {
+        return values.contains(value);
+    }
 
+    /**
+     * Returns {@code true} if this JSONArray contains all of the values
+     * in the specified collection.
+     *
+     * @param c collection to be checked for containment in this collection
+     * @return {@code true} if this collection contains all of the elements
+     *         in the specified collection
+     * @throws NullPointerException
+     *             if the specified collection is null.
+     * @see #contains(Object)
+     * @since 5.7
+     */
+    @Override
+    public boolean containsAll(Collection<?> c)
+    {
+        return values.containsAll(c);
+    }
 }
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java
new file mode 100644
index 0000000..0c26a88
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONExceptionBuilder.java
@@ -0,0 +1,42 @@
+package org.apache.tapestry5.json;
+
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
+class JSONExceptionBuilder
+{
+
+    static RuntimeException typeMismatch(boolean array, Object indexOrName, Object actual,
+            JSONType requiredType)
+    {
+        String location = array ? "JSONArray[" + indexOrName + "]"
+                : "JSONObject[\"" + indexOrName + "\"]";
+        if (actual == null)
+        {
+            return valueNotFound(array, indexOrName, requiredType);
+        }
+        else
+        {
+            return new JSONTypeMismatchException(location, requiredType, actual.getClass());
+        }
+    }
+
+    static RuntimeException valueNotFound(boolean array, Object indexOrName, JSONType requiredType)
+    {
+        String location = array ? "JSONArray[" + indexOrName + "]"
+                : "JSONObject[\"" + indexOrName + "\"]";
+        return new JSONValueNotFoundException(location, requiredType);
+    }
+
+    static RuntimeException tokenerTypeMismatch(Object actual, JSONType requiredType)
+    {
+        if (actual == null)
+        {
+            return new JSONValueNotFoundException("Value", requiredType);
+        }
+        else
+        {
+            return new JSONTypeMismatchException("Value", requiredType, actual.getClass());
+        }
+    }
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
index f5727fa..caf3285 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONObject.java
@@ -18,11 +18,15 @@
 
 import java.io.ObjectStreamException;
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException;
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException;
+
 // Note: this class was written without inspecting the non-free org.json sourcecode.
 
 /**
@@ -71,7 +75,7 @@
  *
  * <p>Instances of this class are not thread safe.
  */
-public final class JSONObject extends JSONCollection {
+public final class JSONObject extends JSONCollection implements Map<String, Object> {
 
     private static final Double NEGATIVE_ZERO = -0d;
 
@@ -146,7 +150,7 @@
         if (object instanceof JSONObject) {
             this.nameValuePairs = ((JSONObject) object).nameValuePairs;
         } else {
-            throw JSON.typeMismatch(object, "JSONObject");
+            throw JSONExceptionBuilder.tokenerTypeMismatch(object, JSONType.OBJECT);
         }
     }
 
@@ -197,7 +201,7 @@
 
     /**
      * Constructs a new JSONObject using a series of String keys and object values.
-     * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings
+     * Object values should be compatible with {@link #put(String, Object)}. Keys must be strings
      * (toString() will be invoked on each key).
      *
      * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes
@@ -219,9 +223,11 @@
 
     /**
      * Returns the number of name/value mappings in this object.
-     *
+     * 
+     * @deprecated Use {@link #size()} instead.
      * @return the length of this.
      */
+    @Deprecated
     public int length() {
         return nameValuePairs.size();
     }
@@ -237,14 +243,15 @@
      *              {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
      *              infinities}.
      * @return this object.
-     * @throws RuntimeException if the value is an invalid double (infinite or NaN).
+     * @throws IllegalArgumentException if the value is an invalid double (infinite or NaN).
      */
+    @Override
     public JSONObject put(String name, Object value) {
         if (value == null) {
             nameValuePairs.remove(name);
             return this;
         }
-        testValidity(value);
+        JSON.testValidity(value);
         if (value instanceof Number) {
             // deviate from the original by checking all Numbers, not just floats & doubles
             JSON.checkDouble(((Number) value).doubleValue());
@@ -303,11 +310,11 @@
      * @param name  The name of the array to which the value should be appended.
      * @param value The value to append.
      * @return this object.
-     * @throws RuntimeException if {@code name} is {@code null} or if the mapping for
+     * @throws JSONTypeMismatchException if {@code name} is {@code null} or if the mapping for
      *                       {@code name} is non-null and is not a {@link JSONArray}.
      */
     public JSONObject append(String name, Object value) {
-        testValidity(value);
+        JSON.testValidity(value);
         Object current = nameValuePairs.get(checkName(name));
 
         final JSONArray array;
@@ -318,7 +325,7 @@
             nameValuePairs.put(name, newArray);
             array = newArray;
         } else {
-            throw new RuntimeException("JSONObject[\"" + name + "\"] is not a JSONArray.");
+            throw new JSONTypeMismatchException("JSONObject[\"" + name + "\"]", JSONType.ARRAY, current.getClass());
         }
 
         array.checkedPut(value);
@@ -334,17 +341,6 @@
     }
 
     /**
-     * Removes the named mapping if it exists; does nothing otherwise.
-     *
-     * @param name The name of the mapping to remove.
-     * @return the value previously mapped by {@code name}, or null if there was
-     * no such mapping.
-     */
-    public Object remove(String name) {
-        return nameValuePairs.remove(name);
-    }
-
-    /**
      * Returns true if this object has no mapping for {@code name} or if it has
      * a mapping whose value is {@link #NULL}.
      *
@@ -360,26 +356,14 @@
      * Returns true if this object has a mapping for {@code name}. The mapping
      * may be {@link #NULL}.
      *
-     * @param name The name of the value to check on.
+     * @deprecated use {@link #containsKey(Object)} instead
+     * @param name
+     *            The name of the value to check on.
      * @return true if this object has a field named {@code name}
      */
+    @Deprecated
     public boolean has(String name) {
-        return nameValuePairs.containsKey(name);
-    }
-
-    /**
-     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
-     *
-     * @param name The name of the value to get.
-     * @return The value.
-     * @throws RuntimeException if no such mapping exists.
-     */
-    public Object get(String name) {
-        Object result = nameValuePairs.get(name);
-        if (result == null) {
-            throw new RuntimeException("JSONObject[\"" + name + "\"] not found.");
-        }
-        return result;
+        return containsKey(name);
     }
 
     /**
@@ -389,7 +373,7 @@
      * @param name The name of the value to get.
      * @return The value.
      */
-    public Object opt(String name) {
+    public Object opt(Object name) {
         return nameValuePairs.get(name);
     }
 
@@ -399,14 +383,45 @@
      *
      * @param name The name of the field we want.
      * @return The selected value if it exists.
-     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a boolean.
      */
     public boolean getBoolean(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.BOOLEAN);
+        }
         Boolean result = JSON.toBoolean(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "Boolean");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the value to which the specified key is mapped and a boolean, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a boolean.
+     * @since 5.7
+     */
+    public boolean getBooleanOrDefault(String name, boolean defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Boolean result = JSON.toBoolean(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.BOOLEAN);
         }
         return result;
     }
@@ -417,14 +432,18 @@
      *
      * @param name The name of the field we want.
      * @return The selected value if it exists.
-     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a double.
      */
     public double getDouble(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Double result = JSON.toDouble(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "number");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -435,19 +454,50 @@
      *
      * @param name The name of the field we want.
      * @return The selected value if it exists.
-     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to an int.
      */
     public int getInt(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Integer result = JSON.toInteger(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "int");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
 
     /**
+     * Returns the value to which the specified key is mapped and an int, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to an int.
+     * @since 5.7
+     */
+    public int getIntOrDefault(String name, int defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Integer result = JSON.toInteger(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
+        }
+        return result;
+    }
+    
+    /**
      * Returns the value mapped by {@code name} if it exists and is a long or
      * can be coerced to a long, or throws otherwise.
      * Note that JSON represents numbers as doubles,
@@ -457,14 +507,45 @@
      *
      * @param name The name of the field that we want.
      * @return The value of the field.
-     * @throws RuntimeException if the mapping doesn't exist or cannot be coerced
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
      *                       to a long.
      */
     public long getLong(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.NUMBER);
+        }
         Long result = JSON.toLong(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "long");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
+        }
+        return result;
+    }
+
+    /**
+     * Returns the value to which the specified key is mapped and a long, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a long.
+     * @since 5.7
+     */
+    public long getLongOrDefault(String name, int defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        Long result = JSON.toLong(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.NUMBER);
         }
         return result;
     }
@@ -475,53 +556,145 @@
      *
      * @param name The name of the field we want.
      * @return The value of the field.
-     * @throws RuntimeException if no such mapping exists.
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                               to String
      */
     public String getString(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.STRING);
+        }
         String result = JSON.toString(object);
         if (result == null) {
-            throw JSON.typeMismatch(false, name, object, "String");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
         }
         return result;
     }
 
     /**
+     * Returns the value to which the specified key is mapped and a string, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a string.
+     * @since 5.7
+     */
+    public String getStringOrDefault(String name, String defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        String result = JSON.toString(object);
+        if (result == null)
+        {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.STRING);
+        }
+        return result;
+    }
+    
+    /**
      * Returns the value mapped by {@code name} if it exists and is a {@code
      * JSONArray}, or throws otherwise.
      *
      * @param name The field we want to get.
      * @return The value of the field (if it is a JSONArray.
-     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping is not a {@code
      *                       JSONArray}.
      */
     public JSONArray getJSONArray(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.ARRAY);
+        }
         if (object instanceof JSONArray) {
             return (JSONArray) object;
         } else {
-            throw JSON.typeMismatch(false, name, object, "JSONArray");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
         }
     }
 
     /**
+     * Returns the value to which the specified key is mapped and a JSONArray, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this JSONObject contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a JSONArray.
+     * @since 5.7
+     */
+    public JSONArray getJSONArrayOrDefault(String name, JSONArray defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        if (object instanceof JSONArray) {
+            return (JSONArray) object;
+        } else {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.ARRAY);
+        }
+    }
+    
+    /**
      * Returns the value mapped by {@code name} if it exists and is a {@code
      * JSONObject}, or throws otherwise.
      *
      * @param name The name of the field that we want.
      * @return a specified field value (if it is a JSONObject)
-     * @throws RuntimeException if the mapping doesn't exist or is not a {@code
+     * @throws JSONValueNotFoundException if the mapping doesn't exist
+     * @throws JSONTypeMismatchException if the mapping is not a {@code
      *                       JSONObject}.
      */
     public JSONObject getJSONObject(String name) {
-        Object object = get(name);
+        Object object = opt(name);
+        if (object == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, name, JSONType.OBJECT);
+        }
         if (object instanceof JSONObject) {
             return (JSONObject) object;
         } else {
-            throw JSON.typeMismatch(false, name, object, "JSONObject");
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
         }
     }
 
+    
+    /**
+     * Returns the value to which the specified key is mapped and a JSONObject, or
+     * {@code defaultValue} if this map contains no mapping for the key.
+     *
+     * @param key the key whose associated value is to be returned
+     * @param defaultValue the default mapping of the key
+     * @return the value to which the specified key is mapped, or
+     * {@code defaultValue} if this map contains no mapping for the key
+     * @throws JSONTypeMismatchException if the mapping cannot be coerced
+     *                       to a JSONObject.
+     * @since 5.7
+     */
+    public JSONObject getJSONObjectOrDefault(String name, JSONObject defaultValue)
+    {
+        Object object = opt(name);
+        if (object == null)
+        {
+            return defaultValue;
+        }
+        if (object instanceof JSONObject) {
+            return (JSONObject) object;
+        } else {
+            throw JSONExceptionBuilder.typeMismatch(false, name, object, JSONType.OBJECT);
+        }
+    }
     /**
      * Returns the set of {@code String} names in this object. The returned set
      * is a view of the keys in this object. {@link Set#remove(Object)} will remove
@@ -731,27 +904,6 @@
     }
 
     /**
-     * Invokes {@link #put(String, Object)} for each value from the map.
-     *
-     * @param newProperties
-     *         to add to this JSONObject
-     * @return this JSONObject
-     * @since 5.4
-     */
-    public JSONObject putAll(Map<String, ?> newProperties)
-    {
-        assert newProperties != null;
-
-        for (Map.Entry<String, ?> e : newProperties.entrySet())
-        {
-            put(e.getKey(), e.getValue());
-        }
-
-        return this;
-    }
-
-
-    /**
      * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist,
      * or must be a JSONObject.
      *
@@ -780,27 +932,180 @@
         return (JSONObject) nested;
     }
 
-    static void testValidity(Object value)
+    /**
+     * Returns the number of key-value mappings in this JSONObject.
+     * If it contains more than {@code Integer.MAX_VALUE} elements, returns
+     * {@code Integer.MAX_VALUE}.
+     *
+     * @return the number of key-value mappings in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public int size()
     {
-        if (value == null)
-          throw new IllegalArgumentException("null isn't valid in JSONObject and JSONArray. Use JSONObject.NULL instead.");
-        if (value == NULL)
-        {
-            return;
-        }
-        Class<? extends Object> clazz = value.getClass();
-        if (Boolean.class.isAssignableFrom(clazz)
-            || Number.class.isAssignableFrom(clazz)
-            || String.class.isAssignableFrom(clazz)
-            || JSONArray.class.isAssignableFrom(clazz)
-            || JSONLiteral.class.isAssignableFrom(clazz)
-            || JSONObject.class.isAssignableFrom(clazz)
-            || JSONString.class.isAssignableFrom(clazz))
-        {
-            return;
-        }
-
-        throw new RuntimeException("JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type "+clazz.getName()+" is not allowed.");
+        return nameValuePairs.size();
     }
 
+    /**
+     * Returns {@code true} if this JSONObject contains no key-value mappings.
+     *
+     * @return {@code true} if this JSONObject contains no key-value mappings
+     * @since 5.7
+     */
+    @Override
+    public boolean isEmpty()
+    {
+        return nameValuePairs.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if this JSONObject contains a mapping for the specified
+     * key.
+     *
+     * @param key
+     *            key whose presence in this map is to be tested
+     * @return {@code true} if this map contains a mapping for the specified
+     *         key
+     * @since 5.7
+     */
+    @Override
+    public boolean containsKey(Object key)
+    {
+        return nameValuePairs.containsKey(key);
+    }
+
+    /**
+     * Returns {@code true} if this JSONObject maps one or more keys to the
+     * specified value.
+     *
+     * @param value value whose presence in this map is to be tested
+     * @return {@code true} if this JSONObject maps one or more keys to the
+     *         specified value
+     * @since 5.7
+     */
+    @Override
+    public boolean containsValue(Object value)
+    {
+        return nameValuePairs.containsValue(value);
+    }
+
+    /**
+     * Returns the value mapped by {@code name}, or throws if no such mapping exists.
+     *
+     * @param name The name of the value to get.
+     * @return The value.
+     * @throws JSONValueNotFoundException if no such mapping exists.
+     */ @Override
+    public Object get(Object key)
+    {
+        Object result = nameValuePairs.get(key);
+         if (result == null) {
+            throw JSONExceptionBuilder.valueNotFound(false, key, JSONType.ANY);
+         }
+         return result;
+    }
+
+    /**
+      * Returns the value to which the specified key is mapped, or
+      * {@code defaultValue} if this JSONObject contains no mapping for the key.
+      *
+      * @param key the key whose associated value is to be returned
+      * @param defaultValue the default mapping of the key
+      * @return the value to which the specified key is mapped, or
+      *         {@code defaultValue} if this JSONObject contains no mapping for the key
+      * @since 5.7
+      */
+    @Override
+    public Object getOrDefault(Object key, Object defaultValue)
+    {
+        Object value = opt(key);
+        return value == null ? defaultValue : value;
+    }
+
+    /**
+     * Removes the named mapping if it exists; does nothing otherwise.
+     *
+     * @param name The name of the mapping to remove.
+     * @return the value previously mapped by {@code name}, or null if there was
+     *         no such mapping.
+     */
+    @Override
+    public Object remove(Object key)
+    {
+        return nameValuePairs.remove(key);
+    }
+
+    /**
+     * Invokes {@link #put(String, Object)} for each value from the map.
+     *
+     * @param newProperties
+     *            to add to this JSONObject
+     * @return this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public void putAll(Map<? extends String, ? extends Object> m)
+    {
+        assert m != null;
+
+        for (Map.Entry<? extends String, ? extends Object> e : m.entrySet())
+        {
+            put(e.getKey(), e.getValue());
+        }
+    }
+
+    /**
+     * Removes all of the mappings from this JSONObject.
+     * 
+     * @since 5.7
+     */
+    @Override
+    public void clear()
+    {
+        nameValuePairs.clear();
+    }
+
+    /**
+     * Returns a {@link Set} view of the keys contained in this JSONObject.
+     * The set is backed by the JSONObject, so changes to the map are
+     * reflected in the set, and vice-versa.
+     *
+     * @return a set view of the keys contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Set<String> keySet()
+    {
+        return nameValuePairs.keySet();
+    }
+
+    /**
+     * Returns a {@link Collection} view of the values contained in this JSONObject.
+     * The collection is backed by the JSONObject, so changes to the map are
+     * reflected in the collection, and vice-versa.
+     *
+     * @return a collection view of the values contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Collection<Object> values()
+    {
+        return nameValuePairs.values();
+    }
+
+    /**
+     * Returns a {@link Set} view of the mappings contained in this JSONObject.
+     * The set is backed by the JSONObject, so changes to the map are
+     * reflected in the set, and vice-versa.
+     *
+     * @return a set view of the mappings contained in this JSONObject
+     * @since 5.7
+     */
+    @Override
+    public Set<Entry<String, Object>> entrySet()
+    {
+        return nameValuePairs.entrySet();
+    }
+  
+
 }
\ No newline at end of file
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
index 38fb443..e0c4dac 100644
--- a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONTokener.java
@@ -20,6 +20,9 @@
 
 import java.io.IOException;
 import java.io.Reader;
+import java.text.MessageFormat;
+
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException;
 
 /**
  * Parses a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
@@ -105,15 +108,15 @@
      *
      * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean,
      * Integer, Long, Double or {@link JSONObject#NULL}.
-     * @throws RuntimeException if the input is malformed.
+     * @throws JSONSyntaxException if the input is malformed.
      */
      Object nextValue(Class<?> desiredType) {
         int c = nextCleanInternal();
         if (JSONObject.class.equals(desiredType) && c != '{'){
-            throw syntaxError("A JSONObject text must begin with '{'");
+            throw syntaxError(MessageFormat.format("A JSONObject text must start with '''{''' (actual: ''{0}'')", Character.toString((char)c)));
         }
         if (JSONArray.class.equals(desiredType) && c != '['){
-          throw syntaxError("A JSONArray text must start with '['");
+          throw syntaxError(MessageFormat.format("A JSONArray text must start with ''['' (actual: ''{0}'')", Character.toString((char)c)));
         }
         switch (c) {
             case -1:
@@ -269,24 +272,28 @@
                     throw syntaxError("Unterminated escape sequence");
                 }
                 String hex = in.substring(pos, pos + 4);
-                pos += 4;
                 try {
                     return (char) Integer.parseInt(hex, 16);
                 } catch (NumberFormatException nfe) {
                     throw syntaxError("Invalid escape sequence: " + hex);
                 }
+                finally {
+                    pos += 4;
+                }
             }
             case 'x': {
               if (pos + 2 > in.length()) {
                   throw syntaxError("Unterminated escape sequence");
               }
               String hex = in.substring(pos, pos + 2);
-              pos += 2;
               try {
                   return (char) Integer.parseInt(hex, 16);
               } catch (NumberFormatException nfe) {
                   throw syntaxError("Invalid escape sequence: " + hex);
               }
+              finally {
+                  pos += 2;
+              }
 
             }
             case 't':
@@ -498,8 +505,8 @@
      * @param message The message we want to include.
      * @return An exception that we can throw.
      */
-    private RuntimeException syntaxError(String message) {
-        return new RuntimeException(message + this);
+    private JSONSyntaxException syntaxError(String message) {
+        return new JSONSyntaxException(this.pos, message + this);
     }
 
     /**
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java
new file mode 100644
index 0000000..7c97f3c
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/JSONType.java
@@ -0,0 +1,11 @@
+package org.apache.tapestry5.json;
+
+/**
+ * Represents the different data types supported by JSON.
+ * Mostly used for descriptive reasons in exceptions.
+ */
+public enum JSONType
+{
+
+    BOOLEAN, STRING, NUMBER, OBJECT, ARRAY, ANY;
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java
new file mode 100644
index 0000000..923e510
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONArrayIndexOutOfBoundsException.java
@@ -0,0 +1,21 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONArrayIndexOutOfBoundsException extends ArrayIndexOutOfBoundsException
+{
+
+    private static final long serialVersionUID = -53336156278974940L;
+
+    private final int index;
+
+    public JSONArrayIndexOutOfBoundsException(int index)
+    {
+        super(index);
+        this.index = index;
+    }
+
+    public int getIndex()
+    {
+        return this.index;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java
new file mode 100644
index 0000000..69873ca
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONInvalidTypeException.java
@@ -0,0 +1,22 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONInvalidTypeException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 934805933638996600L;
+
+    private Class<? extends Object> invalidClass;
+
+    public JSONInvalidTypeException(Class<? extends Object> invalidClass)
+    {
+        super("JSONArray values / JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type "
+                + invalidClass.getName() + " is not allowed.");
+
+        this.invalidClass = invalidClass;
+    }
+
+    public Class<? extends Object> getInvalidClass()
+    {
+        return this.invalidClass;
+    }
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java
new file mode 100644
index 0000000..7323bc3
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONSyntaxException.java
@@ -0,0 +1,21 @@
+package org.apache.tapestry5.json.exceptions;
+
+public class JSONSyntaxException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 5647885303727734937L;
+
+    private final int position;
+
+    public JSONSyntaxException(int position, String message)
+    {
+        super(message);
+        this.position = position;
+    }
+
+    public int getPosition()
+    {
+        return this.position;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java
new file mode 100644
index 0000000..ac16203
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONTypeMismatchException.java
@@ -0,0 +1,39 @@
+package org.apache.tapestry5.json.exceptions;
+
+import org.apache.tapestry5.json.JSONType;
+
+public class JSONTypeMismatchException extends RuntimeException
+{
+
+    private static final long serialVersionUID = 268314880458464132L;
+
+    private final String location;
+    private final JSONType requiredType;
+    private final Class<? extends Object> invalidClass;
+
+    public JSONTypeMismatchException(String location, JSONType requiredType,
+            Class<? extends Object> invalidClass)
+    {
+        super(location + (invalidClass == null ? " is null."
+                : " is not a " + requiredType.name() + ". Actual: " + invalidClass.getName()));
+        this.location = location;
+        this.requiredType = requiredType;
+        this.invalidClass = invalidClass;
+    }
+
+    public String getLocation()
+    {
+        return this.location;
+    }
+
+    public JSONType getRequiredType()
+    {
+        return this.requiredType;
+    }
+
+    public Class<? extends Object> getInvalidClass()
+    {
+        return this.invalidClass;
+    }
+
+}
diff --git a/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java
new file mode 100644
index 0000000..46193dc
--- /dev/null
+++ b/tapestry-json/src/main/java/org/apache/tapestry5/json/exceptions/JSONValueNotFoundException.java
@@ -0,0 +1,30 @@
+package org.apache.tapestry5.json.exceptions;
+
+import org.apache.tapestry5.json.JSONType;
+
+public class JSONValueNotFoundException extends RuntimeException
+{
+
+    private static final long serialVersionUID = -8709125433506778675L;
+
+    private final String location;
+    private final JSONType requiredType;
+
+    public JSONValueNotFoundException(String location, JSONType requiredType)
+    {
+        super(location + " is not found. Required: " + requiredType.name());
+        this.location = location;
+        this.requiredType = requiredType;
+    }
+
+    public String getLocation()
+    {
+        return this.location;
+    }
+
+    public JSONType getRequiredType()
+    {
+        return this.requiredType;
+    }
+
+}
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
index 7cbab1b..35b7658 100644
--- a/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
+++ b/tapestry-json/src/test/groovy/json/specs/JSONArraySpec.groovy
@@ -1,6 +1,13 @@
 package json.specs
 
 import org.apache.tapestry5.json.JSONArray
+import org.apache.tapestry5.json.JSONLiteral
+import org.apache.tapestry5.json.JSONObject
+import org.apache.tapestry5.json.exceptions.JSONArrayIndexOutOfBoundsException
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException
+
 import spock.lang.Specification
 
 class JSONArraySpec extends Specification {
@@ -12,7 +19,7 @@
 
         then:
 
-        array.length() == 3
+        array.size() == 3
         array.get(0) == "foo"
         array.get(1) == "bar"
         array.get(2) == "baz"
@@ -25,7 +32,7 @@
 
         then:
 
-        array.length() == 3
+        array.size() == 3
 
         array.toCompactString() == /["fred","barney","wilma"]/
     }
@@ -37,9 +44,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONSyntaxException e = thrown()
 
-        e.message == "A JSONArray text must start with '[' at character 1 of 1, 2, 3]"
+        e.message == "A JSONArray text must start with '[' (actual: '1') at character 1 of 1, 2, 3]"
     }
 
     def "parse an empty array"() {
@@ -49,7 +56,29 @@
 
         then:
 
+        array.isEmpty()
+    }
+
+    def "isEmpty() is false if array is non-empty"() {
+        when:
+
+        def array = new JSONArray("[1]")
+
+        then:
+
+        array.isEmpty() == false
+    }
+
+    def "isEmpty() == zero length == zero size"() {
+        when:
+
+        def array = new JSONArray("[]")
+
+        then:
+
+        array.isEmpty()
         array.length() == 0
+        array.size() == 0
     }
 
     def "an empty element in the parse is a null"() {
@@ -59,7 +88,7 @@
 
         then:
 
-        array.length() == 3
+        array.size() == 3
         array.getInt(0) == 1
         array.isNull(1)
         array.getInt(2) == 3
@@ -72,7 +101,7 @@
 
         then:
 
-        array.length() == 2
+        array.size() == 2
         array.get(0) == 1
         array.get(1) == 2
     }
@@ -86,9 +115,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[0] is not a Boolean."
+        e.message == "JSONArray[0] is not a BOOLEAN. Actual: java.lang.Integer"
     }
 
     def "handling of boolean values passed into constructor"() {
@@ -114,9 +143,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[0] is not a number."
+        e.message == "JSONArray[0] is not a NUMBER. Actual: java.lang.Boolean"
     }
 
     def "getDouble() works with numbers and parseable strings"() {
@@ -146,9 +175,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[1] is not a JSONArray."
+        e.message == "JSONArray[1] is not a ARRAY. Actual: java.lang.String"
     }
 
     def "get a nested array"() {
@@ -169,9 +198,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == "JSONArray[1] is not a JSONObject."
+        e.message == "JSONArray[1] is not a OBJECT. Actual: java.lang.String"
     }
 
     def "may not put at a negative index"() {
@@ -179,13 +208,13 @@
 
         when:
 
-        array.put(-1, "fred")
+        array.put(-2, "fred")
 
         then:
 
-        RuntimeException e = thrown()
+        JSONArrayIndexOutOfBoundsException e = thrown()
 
-        e.message == "JSONArray[-1] not found."
+        e.index == -2
     }
 
     def "put() overrides existing value in array"() {
@@ -263,7 +292,7 @@
         !i.hasNext()
     }
 
-    def "remove an element"() {
+    def "remove an element by index"() {
         def array = new JSONArray("one", "two", "three")
 
         when:
@@ -272,10 +301,122 @@
 
         then:
 
-        array.length() == 2
+        array.size() == 2
         array.toCompactString() == /["one","three"]/
     }
 
+    def "remove an element by value"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.remove("one")
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["two","three"]/
+    }
+
+    def "remove an element by value - not found"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.remove("four")
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "remove all elements by collection"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["one", "three"])
+
+        then:
+
+        result == true
+        array.size() == 1
+        array.toCompactString() == /["two"]/
+    }
+
+    def "remove all elements by collection - partial"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["one", "four"])
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["two","three"]/
+    }
+
+    def "remove all elements by collection - none found"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.removeAll(["four", "five"])
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "retain all elements by collection - all"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["one", "two", "three"])
+
+        then:
+
+        result == false
+        array.size() == 3
+        array.toCompactString() == /["one","two","three"]/
+    }
+
+    def "retain all elements by collection - partial"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["one", "three"])
+
+        then:
+
+        result == true
+        array.size() == 2
+        array.toCompactString() == /["one","three"]/
+    }
+
+    def "retain all elements by collection - none"() {
+        def array = new JSONArray("one", "two", "three")
+
+        when:
+
+        def result = array.retainAll(["four", "five"])
+
+        then:
+
+        result == true
+        array.isEmpty()
+        array.toCompactString() == /[]/
+    }
+
     def "putAll() adds new objects to existing array"() {
         def array = new JSONArray(100, 200)
 
@@ -300,6 +441,44 @@
         array.toCompactString() == /[100,200]/
     }
 
+
+    def "addAll() adds new objects to existing array"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        array.addAll([300, 400, 500])
+
+        then:
+
+        array.toCompactString() == /[100,200,300,400,500]/
+    }
+
+
+    def "addAll() returns true if changed"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        def result = array.addAll([300, 400, 500])
+
+        then:
+
+        result == true
+    }
+
+    def "addAll() returns false if not changed"() {
+        def array = new JSONArray(100, 200)
+
+        when:
+
+        def result = array.addAll((Collection)null)
+
+        then:
+
+        result == false
+    }
+
     def "list returned by toList() is unmodifiable"() {
         def array = new JSONArray(100, 200)
         def list = array.toList()
@@ -341,32 +520,101 @@
 
         list.toString(true) == "[1,2,3]"
     }
-	
-	def "put() should throw an IllegalArgumentException when receiving null"() {
-		
-		def array = new JSONArray()
-		
-		when:
-		
-		array.put(null)
-		
-		then:
-		
-		thrown IllegalArgumentException
-		
-	}
-	
-	def "new JSONArray() should throw an IllegalArgumentException when receiving null"() {
-		
-		when:
-		
-		new JSONArray(1, null, 3)
-		
-		then:
-		
-		thrown IllegalArgumentException
-		
-	}
 
+    def "put() should throw an IllegalArgumentException when receiving null"() {
 
+        def array = new JSONArray()
+
+        when:
+
+        array.put(null)
+
+        then:
+
+        thrown IllegalArgumentException
+    }
+
+    def "new JSONArray() should throw an IllegalArgumentException when receiving null"() {
+
+        when:
+
+        new JSONArray(1, null, 3)
+
+        then:
+
+        thrown IllegalArgumentException
+    }
+
+    def "only specific object types may be added - no exception"() {
+        def array = new JSONArray()
+
+        when:
+
+        array.put(value)
+
+        then:
+
+        noExceptionThrown()
+
+        where:
+        value << [
+            true,
+            3,
+            3.5,
+            "*VALUE*",
+            new JSONLiteral("*LITERAL*"),
+            new JSONObject(),
+            new JSONArray()
+        ]
+    }
+
+    def "only specific object types may be added - exception"() {
+        def array = new JSONArray()
+
+        when:
+
+        array.put(value)
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        where:
+        value << [
+            new java.util.Date(),
+            [],
+            [:]
+        ]
+    }
+
+    def "array index out of bounds must throw informative exception"() {
+        def array = new JSONArray(1, 2, 3)
+
+        when:
+
+        array.get(array.size())
+
+        then:
+
+        JSONArrayIndexOutOfBoundsException e = thrown()
+        e.index == 3
+    }
+
+    def "non-finite / NaN Double not allowed in constructor"() {
+
+        when:
+
+        new JSONArray(value)
+
+        then:
+
+        RuntimeException e = thrown()
+
+        where:
+        value << [
+            Double.POSITIVE_INFINITY,
+            Double.NEGATIVE_INFINITY,
+            Double.NaN
+        ]
+    }
 }
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
index c2ffba4..e602327 100644
--- a/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
+++ b/tapestry-json/src/test/groovy/json/specs/JSONObjectSpec.groovy
@@ -4,6 +4,11 @@
 import org.apache.tapestry5.json.JSONLiteral
 import org.apache.tapestry5.json.JSONObject
 import org.apache.tapestry5.json.JSONString
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.exceptions.JSONSyntaxException
+import org.apache.tapestry5.json.exceptions.JSONTypeMismatchException
+import org.apache.tapestry5.json.exceptions.JSONValueNotFoundException
+
 import spock.lang.Specification
 import spock.lang.Unroll
 
@@ -82,9 +87,33 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONValueNotFoundException e = thrown()
 
-        e.message == /JSONObject["barney"] not found./
+        e.message == /JSONObject["barney"] is not found. Required: ANY/
+    }
+
+    def "getOrDefault returns defaultValue if not found"() {
+        def master = new JSONObject("fred", "flintstone")
+
+        when:
+
+        def result = master.getOrDefault "barney", "gumble"
+
+        then:
+
+        result == "gumble"
+    }
+
+    def "getOrDefault returns value if found"() {
+        def master = new JSONObject("fred", "flintstone")
+
+        when:
+
+        def result = master.getOrDefault "fred", "other"
+
+        then:
+
+        result == "flintstone"
     }
 
     @Unroll
@@ -148,9 +177,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["akey"] is not a Boolean./
+        e.message == /JSONObject["akey"] is not a BOOLEAN. Actual: java.lang.Integer/
     }
 
     def "accumulate simple values"() {
@@ -238,7 +267,7 @@
         object.toCompactString() == /{"friends":["barney","zaphod"]}/
     }
 
-    def "appending to a key whose value is not a JSONArray is an exception"() {
+    def "appending to a key whose value is not aArray is an exception"() {
         def object = new JSONObject(/{friends: 0 }/)
 
         when:
@@ -247,9 +276,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["friends"] is not a JSONArray./
+        e.message == /JSONObject["friends"] is not a ARRAY. Actual: java.lang.Integer/
     }
 
     def "getDouble() with a non-numeric value is an exception"() {
@@ -261,9 +290,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["notdouble"] is not a number./
+        e.message == /JSONObject["notdouble"] is not a NUMBER. Actual: java.lang.Boolean/
     }
 
     def "getDouble() with a string that can not be parsed as a number is an exception"() {
@@ -275,10 +304,9 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONTypeMismatchException e = thrown()
 
-        e.message == /JSONObject["notdouble"] is not a number./
-
+        e.message == /JSONObject["notdouble"] is not a NUMBER. Actual: java.lang.String/
     }
 
     def "has() will identify which keys have values and which do not"() {
@@ -290,7 +318,7 @@
         !object.has("barney")
     }
 
-    def "getJSONArray() for a value that is not a JSONArray is an exception"() {
+    def "getJSONArray() for a value that is not aArray is an exception"() {
         def object = new JSONObject(/{notarray: 22.7}/)
 
         when:
@@ -301,7 +329,7 @@
 
         RuntimeException e = thrown()
 
-        e.message == /JSONObject["notarray"] is not a JSONArray./
+        e.message == /JSONObject["notarray"] is not a ARRAY. Actual: java.lang.Double/
     }
 
     def "length() of a JSONObject is the number of keys"() {
@@ -400,21 +428,21 @@
 
         then:
 
-        RuntimeException e = thrown()
+        JSONSyntaxException e = thrown()
 
         e.message.trim().startsWith expected
 
         where:
 
-        input                     | expected                                                   | desc
-        "{  "                     | "A JSONObject text must end with '}' at character 3"       | "unmatched open brace"
-        "fred"                    | "A JSONObject text must begin with '{' at character 1 of " | "missing open brace"
-        /{ "akey" }/              | /Expected a ':' after a key at character 10 of/            | "missing value after key"
-        /{ "fred" : 1 "barney" }/ | /Expected a ',' or '}' at character 14 of/                 | "missing property separator"
-        /{ "list" : [1, 2/        | /Expected a ',' or ']' at character 16 of/                 | "missing seperator or closing bracket"
-        '''/* unclosed'''         | /Unclosed comment at character 11 of/                      | "unclosed C-style comment"
-        /{ "fred \n}/             | /Unterminated string at character 11 of/                   | "unterminated string at line end"
-        /{ fred: ,}/              | /Missing value at character 8 of /                         | "missing value after key"
+        input                     | expected                                                                 | desc
+        "{  "                     | "A JSONObject text must end with '}' at character 3"                     | "unmatched open brace"
+        "fred"                    | /A JSONObject text must start with '{' (actual: 'f') at character 1 of/  | "missing open brace"
+        /{ "akey" }/              | /Expected a ':' after a key at character 10 of/                          | "missing value after key"
+        /{ "fred" : 1 "barney" }/ | /Expected a ',' or '}' at character 14 of/                               | "missing property separator"
+        /{ "list" : [1, 2/        | /Expected a ',' or ']' at character 16 of/                               | "missing seperator or closing bracket"
+        '''/* unclosed'''         | /Unclosed comment at character 11 of/                                    | "unclosed C-style comment"
+        /{ "fred \n}/             | /Unterminated string at character 11 of/                                 | "unterminated string at line end"
+        /{ fred: ,}/              | /Missing value at character 8 of /                                       | "missing value after key"
     }
 
     def "can use ':' or '=>' as key seperator, and ';' as property separator"() {
@@ -469,9 +497,14 @@
 
         where:
 
-        value << [Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Float.NaN,
-            Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY]
-
+        value << [
+            Double.NaN,
+            Double.NEGATIVE_INFINITY,
+            Double.POSITIVE_INFINITY,
+            Float.NaN,
+            Float.NEGATIVE_INFINITY,
+            Float.POSITIVE_INFINITY
+        ]
     }
 
     def "parse an empty object is empty"() {
@@ -496,16 +529,47 @@
         object.toCompactString() == /{"barney":"rubble"}/
     }
 
-    def "only specific object types may be added"() {
+    def "only specific object types may be added - no exception"() {
+        def object = new JSONObject(/{}/)
+
         when:
 
-        JSONObject.testValidity([:])
+        object.put("key", value)
 
         then:
 
-        RuntimeException e = thrown()
+        noExceptionThrown()
 
-        e.message == '''JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type java.util.LinkedHashMap is not allowed.'''
+        where:
+        value << [
+            null,
+            true,
+            3,
+            3.5,
+            "*VALUE*",
+            new JSONLiteral("*LITERAL*"),
+            new JSONObject(),
+            new JSONArray()
+        ]
+    }
+
+    def "only specific object types may be added - exception"() {
+        def object = new JSONObject(/{}/)
+
+        when:
+
+        object.put("key", value)
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        where:
+        value << [
+            new java.util.Date(),
+            [],
+            [:]
+        ]
     }
 
     def "JSONString can output anything it wants"() {
@@ -651,7 +715,6 @@
         then:
 
         object.get("foo") == "bar"
-
     }
 
     def "pretty-print an empty JSONObject"() {
@@ -741,7 +804,6 @@
         json == /{
   "kermit" : "frog"
 }/
-
     }
 
     def "getString() at index"() {
@@ -770,8 +832,8 @@
 
     def "can access contents of object as a map"() {
         def object = new JSONObject("foo", "bar")
-            .put("null", JSONObject.NULL)
-            .put("number", 6)
+                .put("null", JSONObject.NULL)
+                .put("number", 6)
 
         when:
 
@@ -808,7 +870,6 @@
 
         then:
 
-        result.is object
         object.toCompactString() == /{"fred":"flintstone","barney":"rubble","wilma":"flintstone"}/
     }
 
@@ -884,7 +945,5 @@
         then:
 
         source == copy
-
     }
-
 }
diff --git a/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy b/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy
new file mode 100644
index 0000000..5b8ae3f
--- /dev/null
+++ b/tapestry-json/src/test/groovy/json/specs/JSONSpec.groovy
@@ -0,0 +1,36 @@
+package json.specs
+
+import org.apache.tapestry5.json.JSONArray
+import org.apache.tapestry5.json.JSONLiteral
+import org.apache.tapestry5.json.JSONObject
+import org.apache.tapestry5.json.JSONString
+import org.apache.tapestry5.json.exceptions.JSONInvalidTypeException
+import org.apache.tapestry5.json.JSON
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class JSONSpec extends Specification {
+
+    def "invalid types throw JSONInvalidTypeException"() {
+        when:
+
+        JSON.testValidity([:])
+
+        then:
+
+        JSONInvalidTypeException e = thrown()
+
+        e.message == '''JSONArray values / JSONObject properties may be one of Boolean, Number, String, org.apache.tapestry5.json.JSONArray, org.apache.tapestry5.json.JSONLiteral, org.apache.tapestry5.json.JSONObject, org.apache.tapestry5.json.JSONObject$Null, org.apache.tapestry5.json.JSONString. Type java.util.LinkedHashMap is not allowed.'''
+    }
+
+   def "null is invalid"() {
+        when:
+
+        JSON.testValidity(null)
+
+        then:
+
+        IllegalArgumentException e = thrown()
+    }
+
+}