Migrating uses of JSONObject and JSONArray

The LabKey server-side source includes widely used JSONObject and JSONArray classes that were forked from org.json sources 15+ years ago. We are replacing this old implementation with the current, maintained json.org JSON library, however, these libraries share the same package and the new version is not a drop-in replacement. To allow both libraries to co-exist for a transition time we have repackaged the old library to org.json.old.* and have added the new library which lives in the org.json.* package. We have begun migrating from old to new in the LabKey-managed repositories, but any code outside these repositories that use JSONObject or JSONArray will likely need changes.

There are some significant differences between the old and new libraries, requiring you to review and revise existing uses. Key differences are summarized below.

JSONObject

The new JSONObject has some significant differences:

  • JSONObject continues to provide a Map-like interface (with put(), get(), remove(), clear(), isEmpty(), etc.) but it no longer implements Map. Instead of treating myJsonObject as a Map, you'll need to call toMap() to generate a new Map. Note that the generated Map does NOT hold JSONObject and JSONArray values, as you used to find in the old map-like JSONObject; instead, it provides compound values as generic Maps and Lists. When working with a converted Map, you may need to update casts and instanceof checks (or switch to operating directly on the JSONObject with typed getters).
  • Important behavior change: Behavior of the get() method has changed. At some point, we modified our forked implementation to return null if the key was not found; the new version always throws if the key is not found! The typed getters (getString(), getInt(), getJSONArray(), getJSONObject(), etc.) also throw for not found, but their behavior hasn't changed. get*() methods should be used only for properties that are guaranteed to be present. Use opt*() methods for optional properties (but be aware of the default values they return).
  • To handle optional properties, the opt() method returns the value or null if no value is associated with the key. There are also typed versions optString(), optInteger(), optBoolean(), etc. that return and cast the value OR return a default value if no value exists. WARNING: optString() returns an empty String by default... which is not expected or helpful (IMO). All of these methods have a variant where you can specify the default value; in most cases, we use optString("myProp", null) instead of optString("myProp").
  • Best practice: if possible, use the typed versions of get*() and opt*() instead of using get() and opt() and casting the result.
  • Important behavior change: Putting a null value into the new JSONObject removes the previous mapping, if it exists. Either way, the property will not appear in the JSON. Previously, the JSON would include {"propName": null}, in keeping with HashMap behavior. Constructing a new JSONObject from a Map has similar behavior; removing any entries with a null value. To put a null value into a JSONObject, use JSONObject.NULL or JSONObject.wrap(value). Also see JsonUtil.toJsonPreserveNulls() to convert a Map<String, Object> to a JSONObject while preserving null values.
  • Method change: myJsonObject.containsKey() -> myJsonObject.has()
  • No putAll() method: myJsonObject.putAll(map) -> map.forEach(myJsonObject::put)
  • Method change for pretty printing from a JSP: myJsonObject.getJavaScriptFragment(indentFactor) -> JspBase.json(myJsonObject, indentFactor)
  • The new version no longer implements SafeToRender but JSONObjects will still render directly from JSPs (i.e., <%=myJsonObject%> is legal). (LabKeyJspWriter special cases this class as it does with Number and Boolean.)
  • The new version doesn't implement a meaningful equals() method, so avoid directly comparing these objects with each other. There is a similar() method that could be a good comparison option. Comparing the results of JSONObject.toMap() is an option as well, though likely more expensive.
  • The new library no longer supports comments in JSON source. While comments are found in some JSON documents, they are not allowed by the official standard. JsonUtil.stripComments() can be used to remove these comments before parsing with new JSONObject(String) or similar.
  • The new JSONObject is more aggressive when serializing Java beans compared with the old class. The new class will drill into more getters, often yielding more verbose output and, in some cases, recursion and cyclic definition exceptions. Annotate problematic getters with the @JSONPropertyIgnore annotation to avoid these problems. Or, if generating a serialization map via BeanObjectFactory, annotate with @Transient.

In short, the method and class changes are fairly obvious and simple; the get() / opt() behavior, null value handling, and the new non-Map implementation are the changes that require careful attention.

JSONArray

The new JSONArray is very similar to the old JSONArray, but there are some differences to note:

  • Method change: JSONArray.collector() -> LabKeyCollectors.toJSONArray()
  • Method change for pretty printing from a JSP: myJsonArray.getJavaScriptFragment(indentFactor) -> JspBase.json(myJsonArray, indentFactor)
  • The new version no longer implements SafeToRender but JSONArray objects will still render directly from JSPs (i.e., <%=myJsonArray%> is legal). (LabKeyJspWriter special cases this class as it does with Number and Boolean.)
  • The new version does not implement equals() or hashCode(). Avoid comparing JSONArrays or putting them in hash-based data structures. There is a similar() method that could be a good comparison option.
  • Old JSONArray instance methods toJSONObjectArray() and toMapList() are now available as JsonUtil static methods.
  • Method change: JSONArray.toArray() -> JSONArray.toList()
  • Behavior change: new JSONArray.getString() throws if the value is not a String; old version was simply get().toString().
  • The new library no longer supports comments in JSON source. While comments are found in some JSON documents, they are not allowed by the official standard. JsonUtil.stripComments() can be used to remove these comments before parsing with new JSONArray(String) or similar.

1 Related Topics

  • [Development Resources|dev]

Was this content helpful?

Log in or register an account to provide feedback


previousnext
 
expand allcollapse all