001/* 002 * Copyright (C) 2010 The Android Open Source Project 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.apache.tapestry5.json; 018 019import java.io.ObjectStreamException; 020import java.io.Serializable; 021import java.util.Collections; 022import java.util.LinkedHashMap; 023import java.util.Map; 024import java.util.Set; 025 026// Note: this class was written without inspecting the non-free org.json sourcecode. 027 028/** 029 * A modifiable set of name/value mappings. Names are unique, non-null strings. 030 * Values may be any mix of {@link JSONObject JSONObjects}, {@link JSONArray 031 * JSONArrays}, Strings, Booleans, Integers, Longs, Doubles or {@link #NULL}. 032 * Values may not be {@code null}, {@link Double#isNaN() NaNs}, {@link 033 * Double#isInfinite() infinities}, or of any type not listed here. 034 * 035 * <p>This class can coerce values to another type when requested. 036 * <ul> 037 * <li>When the requested type is a boolean, strings will be coerced using a 038 * case-insensitive comparison to "true" and "false". 039 * <li>When the requested type is a double, other {@link Number} types will 040 * be coerced using {@link Number#doubleValue() doubleValue}. Strings 041 * that can be coerced using {@link Double#valueOf(String)} will be. 042 * <li>When the requested type is an int, other {@link Number} types will 043 * be coerced using {@link Number#intValue() intValue}. Strings 044 * that can be coerced using {@link Double#valueOf(String)} will be, 045 * and then cast to int. 046 * <li><a name="lossy">When the requested type is a long, other {@link Number} types will 047 * be coerced using {@link Number#longValue() longValue}. Strings 048 * that can be coerced using {@link Double#valueOf(String)} will be, 049 * and then cast to long. This two-step conversion is lossy for very 050 * large values. For example, the string "9223372036854775806" yields the 051 * long 9223372036854775807.</a> 052 * <li>When the requested type is a String, other non-null values will be 053 * coerced using {@link String#valueOf(Object)}. Although null cannot be 054 * coerced, the sentinel value {@link JSONObject#NULL} is coerced to the 055 * string "null". 056 * </ul> 057 * 058 * <p>This class can look up both mandatory and optional values: 059 * <ul> 060 * <li>Use <code>get<i>Type</i>()</code> to retrieve a mandatory value. This 061 * fails with a {@code RuntimeException} if the requested name has no value 062 * or if the value cannot be coerced to the requested type. 063 * <li>Use <code>opt()</code> to retrieve an optional value. 064 * </ul> 065 * 066 * <p><strong>Warning:</strong> this class represents null in two incompatible 067 * ways: the standard Java {@code null} reference, and the sentinel value {@link 068 * JSONObject#NULL}. In particular, calling {@code put(name, null)} removes the 069 * named entry from the object but {@code put(name, JSONObject.NULL)} stores an 070 * entry whose value is {@code JSONObject.NULL}. 071 * 072 * <p>Instances of this class are not thread safe. 073 */ 074public final class JSONObject extends JSONCollection { 075 076 private static final Double NEGATIVE_ZERO = -0d; 077 078 /** 079 * A sentinel value used to explicitly define a name with no value. Unlike 080 * {@code null}, names with this value: 081 * <ul> 082 * <li>show up in the {@link #names} array 083 * <li>show up in the {@link #keys} iterator 084 * <li>return {@code true} for {@link #has(String)} 085 * <li>do not throw on {@link #get(String)} 086 * <li>are included in the encoded JSON string. 087 * </ul> 088 * 089 * <p>This value violates the general contract of {@link Object#equals} by 090 * returning true when compared to {@code null}. Its {@link #toString} 091 * method returns "null". 092 */ 093 public static final Object NULL = new Serializable() { 094 095 private static final long serialVersionUID = 1L; 096 097 @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") 098 @Override 099 public boolean equals(Object o) { 100 return o == this || o == null; // API specifies this broken equals implementation 101 } 102 103 // at least make the broken equals(null) consistent with Objects.hashCode(null). 104 @Override 105 public int hashCode() { 106 return 0; 107 } 108 109 @Override 110 public String toString() { 111 return "null"; 112 } 113 114 // Serialization magic: after de-serializing, it will be back to the singleton instance of NULL. 115 private Object readResolve() throws ObjectStreamException 116 { 117 return NULL; 118 } 119 120 }; 121 122 private final LinkedHashMap<String, Object> nameValuePairs; 123 124 /** 125 * Creates a {@code JSONObject} with no name/value mappings. 126 */ 127 public JSONObject() { 128 nameValuePairs = new LinkedHashMap<String, Object>(); 129 } 130 131 /** 132 * Creates a new {@code JSONObject} with name/value mappings from the next 133 * object in the tokener. 134 * 135 * @param readFrom a tokener whose nextValue() method will yield a 136 * {@code JSONObject}. 137 * @throws RuntimeException if the parse fails or doesn't yield a 138 * {@code JSONObject}. 139 */ 140 JSONObject(JSONTokener readFrom) { 141 /* 142 * Getting the parser to populate this could get tricky. Instead, just 143 * parse to temporary JSONObject and then steal the data from that. 144 */ 145 Object object = readFrom.nextValue(JSONObject.class); 146 if (object instanceof JSONObject) { 147 this.nameValuePairs = ((JSONObject) object).nameValuePairs; 148 } else { 149 throw JSON.typeMismatch(object, "JSONObject"); 150 } 151 } 152 153 /** 154 * Creates a new {@code JSONObject} with name/value mappings from the JSON 155 * string. 156 * 157 * @param json a JSON-encoded string containing an object. 158 * @throws RuntimeException if the parse fails or doesn't yield a {@code 159 * JSONObject}. 160 */ 161 public JSONObject(String json) { 162 this(new JSONTokener(json)); 163 } 164 165 /** 166 * Creates a new {@code JSONObject} by copying mappings for the listed names 167 * from the given object. Names that aren't present in {@code copyFrom} will 168 * be skipped. 169 * 170 * @param copyFrom The source object. 171 * @param names The names of the fields to copy. 172 * @throws RuntimeException On internal errors. Shouldn't happen. 173 */ 174 public JSONObject(JSONObject copyFrom, String... names) { 175 this(); 176 for (String name : names) { 177 Object value = copyFrom.opt(name); 178 if (value != null) { 179 nameValuePairs.put(name, value); 180 } 181 } 182 } 183 184 185 /** 186 * Returns a new JSONObject that is a shallow copy of this JSONObject. 187 * 188 * @since 5.4 189 */ 190 public JSONObject copy() 191 { 192 JSONObject dupe = new JSONObject(); 193 dupe.nameValuePairs.putAll(nameValuePairs); 194 195 return dupe; 196 } 197 198 /** 199 * Constructs a new JSONObject using a series of String keys and object values. 200 * Object values sholuld be compatible with {@link #put(String, Object)}. Keys must be strings 201 * (toString() will be invoked on each key). 202 * 203 * Prior to release 5.4, keysAndValues was type String...; changing it to Object... makes 204 * it much easier to initialize a JSONObject in a single statement, which is more readable. 205 * 206 * @since 5.2.0 207 */ 208 public JSONObject(Object... keysAndValues) 209 { 210 this(); 211 212 int i = 0; 213 214 while (i < keysAndValues.length) 215 { 216 put(keysAndValues[i++].toString(), keysAndValues[i++]); 217 } 218 } 219 220 /** 221 * Returns the number of name/value mappings in this object. 222 * 223 * @return the length of this. 224 */ 225 public int length() { 226 return nameValuePairs.size(); 227 } 228 229 /** 230 * Maps {@code name} to {@code value}, clobbering any existing name/value 231 * mapping with the same name. If the value is {@code null}, any existing 232 * mapping for {@code name} is removed. 233 * 234 * @param name The name of the new value. 235 * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, 236 * Integer, Long, Double, {@link #NULL}, or {@code null}. May not be 237 * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() 238 * infinities}. 239 * @return this object. 240 * @throws RuntimeException if the value is an invalid double (infinite or NaN). 241 */ 242 public JSONObject put(String name, Object value) { 243 if (value == null) { 244 nameValuePairs.remove(name); 245 return this; 246 } 247 testValidity(value); 248 if (value instanceof Number) { 249 // deviate from the original by checking all Numbers, not just floats & doubles 250 JSON.checkDouble(((Number) value).doubleValue()); 251 } 252 nameValuePairs.put(checkName(name), value); 253 return this; 254 } 255 256 /** 257 * Appends {@code value} to the array already mapped to {@code name}. If 258 * this object has no mapping for {@code name}, this inserts a new mapping. 259 * If the mapping exists but its value is not an array, the existing 260 * and new values are inserted in order into a new array which is itself 261 * mapped to {@code name}. In aggregate, this allows values to be added to a 262 * mapping one at a time. 263 * 264 * Note that {@code append(String, Object)} provides better semantics. 265 * In particular, the mapping for {@code name} will <b>always</b> be a 266 * {@link JSONArray}. Using {@code accumulate} will result in either a 267 * {@link JSONArray} or a mapping whose type is the type of {@code value} 268 * depending on the number of calls to it. 269 * 270 * @param name The name of the field to change. 271 * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, 272 * Integer, Long, Double, {@link #NULL} or null. May not be {@link 273 * Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. 274 * @return this object after mutation. 275 * @throws RuntimeException If the object being added is an invalid number. 276 */ 277 // TODO: Change {@code append) to {@link #append} when append is 278 // unhidden. 279 public JSONObject accumulate(String name, Object value) { 280 Object current = nameValuePairs.get(checkName(name)); 281 if (current == null) { 282 return put(name, value); 283 } 284 285 if (current instanceof JSONArray) { 286 JSONArray array = (JSONArray) current; 287 array.checkedPut(value); 288 } else { 289 JSONArray array = new JSONArray(); 290 array.checkedPut(current); 291 array.checkedPut(value); 292 nameValuePairs.put(name, array); 293 } 294 return this; 295 } 296 297 /** 298 * Appends values to the array mapped to {@code name}. A new {@link JSONArray} 299 * mapping for {@code name} will be inserted if no mapping exists. If the existing 300 * mapping for {@code name} is not a {@link JSONArray}, a {@link RuntimeException} 301 * will be thrown. 302 * 303 * @param name The name of the array to which the value should be appended. 304 * @param value The value to append. 305 * @return this object. 306 * @throws RuntimeException if {@code name} is {@code null} or if the mapping for 307 * {@code name} is non-null and is not a {@link JSONArray}. 308 */ 309 public JSONObject append(String name, Object value) { 310 testValidity(value); 311 Object current = nameValuePairs.get(checkName(name)); 312 313 final JSONArray array; 314 if (current instanceof JSONArray) { 315 array = (JSONArray) current; 316 } else if (current == null) { 317 JSONArray newArray = new JSONArray(); 318 nameValuePairs.put(name, newArray); 319 array = newArray; 320 } else { 321 throw new RuntimeException("JSONObject[\"" + name + "\"] is not a JSONArray."); 322 } 323 324 array.checkedPut(value); 325 326 return this; 327 } 328 329 String checkName(String name) { 330 if (name == null) { 331 throw new RuntimeException("Names must be non-null"); 332 } 333 return name; 334 } 335 336 /** 337 * Removes the named mapping if it exists; does nothing otherwise. 338 * 339 * @param name The name of the mapping to remove. 340 * @return the value previously mapped by {@code name}, or null if there was 341 * no such mapping. 342 */ 343 public Object remove(String name) { 344 return nameValuePairs.remove(name); 345 } 346 347 /** 348 * Returns true if this object has no mapping for {@code name} or if it has 349 * a mapping whose value is {@link #NULL}. 350 * 351 * @param name The name of the value to check on. 352 * @return true if the field doesn't exist or is null. 353 */ 354 public boolean isNull(String name) { 355 Object value = nameValuePairs.get(name); 356 return value == null || value == NULL; 357 } 358 359 /** 360 * Returns true if this object has a mapping for {@code name}. The mapping 361 * may be {@link #NULL}. 362 * 363 * @param name The name of the value to check on. 364 * @return true if this object has a field named {@code name} 365 */ 366 public boolean has(String name) { 367 return nameValuePairs.containsKey(name); 368 } 369 370 /** 371 * Returns the value mapped by {@code name}, or throws if no such mapping exists. 372 * 373 * @param name The name of the value to get. 374 * @return The value. 375 * @throws RuntimeException if no such mapping exists. 376 */ 377 public Object get(String name) { 378 Object result = nameValuePairs.get(name); 379 if (result == null) { 380 throw new RuntimeException("JSONObject[\"" + name + "\"] not found."); 381 } 382 return result; 383 } 384 385 /** 386 * Returns the value mapped by {@code name}, or null if no such mapping 387 * exists. 388 * 389 * @param name The name of the value to get. 390 * @return The value. 391 */ 392 public Object opt(String name) { 393 return nameValuePairs.get(name); 394 } 395 396 /** 397 * Returns the value mapped by {@code name} if it exists and is a boolean or 398 * can be coerced to a boolean, or throws otherwise. 399 * 400 * @param name The name of the field we want. 401 * @return The selected value if it exists. 402 * @throws RuntimeException if the mapping doesn't exist or cannot be coerced 403 * to a boolean. 404 */ 405 public boolean getBoolean(String name) { 406 Object object = get(name); 407 Boolean result = JSON.toBoolean(object); 408 if (result == null) { 409 throw JSON.typeMismatch(false, name, object, "Boolean"); 410 } 411 return result; 412 } 413 414 /** 415 * Returns the value mapped by {@code name} if it exists and is a double or 416 * can be coerced to a double, or throws otherwise. 417 * 418 * @param name The name of the field we want. 419 * @return The selected value if it exists. 420 * @throws RuntimeException if the mapping doesn't exist or cannot be coerced 421 * to a double. 422 */ 423 public double getDouble(String name) { 424 Object object = get(name); 425 Double result = JSON.toDouble(object); 426 if (result == null) { 427 throw JSON.typeMismatch(false, name, object, "number"); 428 } 429 return result; 430 } 431 432 /** 433 * Returns the value mapped by {@code name} if it exists and is an int or 434 * can be coerced to an int, or throws otherwise. 435 * 436 * @param name The name of the field we want. 437 * @return The selected value if it exists. 438 * @throws RuntimeException if the mapping doesn't exist or cannot be coerced 439 * to an int. 440 */ 441 public int getInt(String name) { 442 Object object = get(name); 443 Integer result = JSON.toInteger(object); 444 if (result == null) { 445 throw JSON.typeMismatch(false, name, object, "int"); 446 } 447 return result; 448 } 449 450 /** 451 * Returns the value mapped by {@code name} if it exists and is a long or 452 * can be coerced to a long, or throws otherwise. 453 * Note that JSON represents numbers as doubles, 454 * 455 * so this is <a href="#lossy">lossy</a>; use strings to transfer numbers 456 * via JSON without loss. 457 * 458 * @param name The name of the field that we want. 459 * @return The value of the field. 460 * @throws RuntimeException if the mapping doesn't exist or cannot be coerced 461 * to a long. 462 */ 463 public long getLong(String name) { 464 Object object = get(name); 465 Long result = JSON.toLong(object); 466 if (result == null) { 467 throw JSON.typeMismatch(false, name, object, "long"); 468 } 469 return result; 470 } 471 472 /** 473 * Returns the value mapped by {@code name} if it exists, coercing it if 474 * necessary, or throws if no such mapping exists. 475 * 476 * @param name The name of the field we want. 477 * @return The value of the field. 478 * @throws RuntimeException if no such mapping exists. 479 */ 480 public String getString(String name) { 481 Object object = get(name); 482 String result = JSON.toString(object); 483 if (result == null) { 484 throw JSON.typeMismatch(false, name, object, "String"); 485 } 486 return result; 487 } 488 489 /** 490 * Returns the value mapped by {@code name} if it exists and is a {@code 491 * JSONArray}, or throws otherwise. 492 * 493 * @param name The field we want to get. 494 * @return The value of the field (if it is a JSONArray. 495 * @throws RuntimeException if the mapping doesn't exist or is not a {@code 496 * JSONArray}. 497 */ 498 public JSONArray getJSONArray(String name) { 499 Object object = get(name); 500 if (object instanceof JSONArray) { 501 return (JSONArray) object; 502 } else { 503 throw JSON.typeMismatch(false, name, object, "JSONArray"); 504 } 505 } 506 507 /** 508 * Returns the value mapped by {@code name} if it exists and is a {@code 509 * JSONObject}, or throws otherwise. 510 * 511 * @param name The name of the field that we want. 512 * @return a specified field value (if it is a JSONObject) 513 * @throws RuntimeException if the mapping doesn't exist or is not a {@code 514 * JSONObject}. 515 */ 516 public JSONObject getJSONObject(String name) { 517 Object object = get(name); 518 if (object instanceof JSONObject) { 519 return (JSONObject) object; 520 } else { 521 throw JSON.typeMismatch(false, name, object, "JSONObject"); 522 } 523 } 524 525 /** 526 * Returns the set of {@code String} names in this object. The returned set 527 * is a view of the keys in this object. {@link Set#remove(Object)} will remove 528 * the corresponding mapping from this object and set iterator behaviour 529 * is undefined if this object is modified after it is returned. 530 * 531 * See {@link #keys()}. 532 * 533 * @return The names in this object. 534 */ 535 public Set<String> keys() { 536 return nameValuePairs.keySet(); 537 } 538 539 /** 540 * Returns an array containing the string names in this object. This method 541 * returns null if this object contains no mappings. 542 * 543 * @return the names. 544 */ 545 public JSONArray names() { 546 return nameValuePairs.isEmpty() 547 ? null 548 : JSONArray.from(nameValuePairs.keySet()); 549 } 550 551 /** 552 * Encodes the number as a JSON string. 553 * 554 * @param number a finite value. May not be {@link Double#isNaN() NaNs} or 555 * {@link Double#isInfinite() infinities}. 556 * @return The encoded number in string form. 557 * @throws RuntimeException On internal errors. Shouldn't happen. 558 */ 559 public static String numberToString(Number number) { 560 if (number == null) { 561 throw new RuntimeException("Number must be non-null"); 562 } 563 564 double doubleValue = number.doubleValue(); 565 JSON.checkDouble(doubleValue); 566 567 // the original returns "-0" instead of "-0.0" for negative zero 568 if (number.equals(NEGATIVE_ZERO)) { 569 return "-0"; 570 } 571 572 long longValue = number.longValue(); 573 if (doubleValue == (double) longValue) { 574 return Long.toString(longValue); 575 } 576 577 return number.toString(); 578 } 579 580 static String doubleToString(double d) 581 { 582 if (Double.isInfinite(d) || Double.isNaN(d)) 583 { 584 return "null"; 585 } 586 587 return numberToString(d); 588 } 589 590 /** 591 * Encodes {@code data} as a JSON string. This applies quotes and any 592 * necessary character escaping. 593 * 594 * @param data the string to encode. Null will be interpreted as an empty 595 * string. 596 * @return the quoted string. 597 */ 598 public static String quote(String data) { 599 if (data == null) { 600 return "\"\""; 601 } 602 try { 603 JSONStringer stringer = new JSONStringer(); 604 stringer.open(JSONStringer.Scope.NULL, ""); 605 stringer.string(data); 606 stringer.close(JSONStringer.Scope.NULL, JSONStringer.Scope.NULL, ""); 607 return stringer.toString(); 608 } catch (RuntimeException e) { 609 throw new AssertionError(); 610 } 611 } 612 613 614 615 /** 616 * Prints the JSONObject using the session. 617 * 618 * @since 5.2.0 619 */ 620 @Override 621 void print(JSONPrintSession session) 622 { 623 session.printSymbol('{'); 624 625 session.indent(); 626 627 boolean comma = false; 628 629 for (String key : keys()) 630 { 631 if (comma) 632 session.printSymbol(','); 633 634 session.newline(); 635 636 session.printQuoted(key); 637 638 session.printSymbol(':'); 639 640 printValue(session, nameValuePairs.get(key)); 641 642 comma = true; 643 } 644 645 session.outdent(); 646 647 if (comma) 648 session.newline(); 649 650 session.printSymbol('}'); 651 } 652 653 654 /** 655 * Prints a value (a JSONArray or JSONObject, or a value stored in an array or object) using 656 * the session. 657 * 658 * @since 5.2.0 659 */ 660 static void printValue(JSONPrintSession session, Object value) 661 { 662 663 if (value == null || value == NULL) 664 { 665 session.print("null"); 666 return; 667 } 668 if (value instanceof JSONObject) 669 { 670 ((JSONObject) value).print(session); 671 return; 672 } 673 674 if (value instanceof JSONArray) 675 { 676 ((JSONArray) value).print(session); 677 return; 678 } 679 680 if (value instanceof JSONString) 681 { 682 String printValue = ((JSONString) value).toJSONString(); 683 684 session.print(printValue); 685 686 return; 687 } 688 689 if (value instanceof Number) 690 { 691 String printValue = numberToString((Number) value); 692 session.print(printValue); 693 return; 694 } 695 696 if (value instanceof Boolean) 697 { 698 session.print(value.toString()); 699 700 return; 701 } 702 703 // Otherwise it really should just be a string. Nothing else can go in. 704 session.printQuoted(value.toString()); 705 } 706 707 public boolean equals(Object obj) 708 { 709 if (obj == null) 710 return false; 711 712 if (!(obj instanceof JSONObject)) 713 return false; 714 715 JSONObject other = (JSONObject) obj; 716 717 return nameValuePairs.equals(other.nameValuePairs); 718 } 719 720 /** 721 * Returns a Map of the keys and values of the JSONObject. The returned map is unmodifiable. 722 * Note that changes to the JSONObject will be reflected in the map. In addition, null values in the JSONObject 723 * are represented as {@link JSONObject#NULL} in the map. 724 * 725 * @return unmodifiable map of properties and values 726 * @since 5.4 727 */ 728 public Map<String, Object> toMap() 729 { 730 return Collections.unmodifiableMap(nameValuePairs); 731 } 732 733 /** 734 * Invokes {@link #put(String, Object)} for each value from the map. 735 * 736 * @param newProperties 737 * to add to this JSONObject 738 * @return this JSONObject 739 * @since 5.4 740 */ 741 public JSONObject putAll(Map<String, ?> newProperties) 742 { 743 assert newProperties != null; 744 745 for (Map.Entry<String, ?> e : newProperties.entrySet()) 746 { 747 put(e.getKey(), e.getValue()); 748 } 749 750 return this; 751 } 752 753 754 /** 755 * Navigates into a nested JSONObject, creating the JSONObject if necessary. They key must not exist, 756 * or must be a JSONObject. 757 * 758 * @param key 759 * @return the nested JSONObject 760 * @throws IllegalStateException 761 * if the current value for the key is not null and not JSONObject 762 */ 763 public JSONObject in(String key) 764 { 765 assert key != null; 766 767 Object nested = nameValuePairs.get(key); 768 769 if (nested != null && !(nested instanceof JSONObject)) 770 { 771 throw new IllegalStateException(String.format("JSONObject[%s] is not a JSONObject.", quote(key))); 772 } 773 774 if (nested == null) 775 { 776 nested = new JSONObject(); 777 nameValuePairs.put(key, nested); 778 } 779 780 return (JSONObject) nested; 781 } 782 783 static void testValidity(Object value) 784 { 785 if (value == null) 786 throw new IllegalArgumentException("null isn't valid in JSONObject and JSONArray. Use JSONObject.NULL instead."); 787 if (value == NULL) 788 { 789 return; 790 } 791 Class<? extends Object> clazz = value.getClass(); 792 if (Boolean.class.isAssignableFrom(clazz) 793 || Number.class.isAssignableFrom(clazz) 794 || String.class.isAssignableFrom(clazz) 795 || JSONArray.class.isAssignableFrom(clazz) 796 || JSONLiteral.class.isAssignableFrom(clazz) 797 || JSONObject.class.isAssignableFrom(clazz) 798 || JSONString.class.isAssignableFrom(clazz)) 799 { 800 return; 801 } 802 803 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."); 804 } 805 806}