We found that we had accumulated four libraries for generating/parsing JSON:
In fact, the most-commonly-used library was the old json-lib, which includes some entertaining misfeatures. Consider adding a property to an object with a string value:
@Test public void put_adds_a_string_to_an_object() {
JSONObject obj = new JSONObject();
String value = "value";
obj.put("prop", value);
assertThat(obj.get("prop"), is(equalTo(value)));
assertThat(obj.toString(), is(equalTo("{\"prop\":\"value\"}")));
}
That string might "magically" be converted to an object, just because it looks like JSON (so every string put feeds the value to the JSON parser):
@Test public void put_adds_raw_json_to_an_object() {
JSONObject obj = new JSONObject();
String value = "{ \"a\": 1 }";
obj.put("prop", value);
assertThat(obj.get("prop"), is(not(equalTo(value))));
assertThat(obj.get("prop"), is(equalTo(JSONObject.fromObject(value))));
assertThat(obj.toString(), is(equalTo("{\"prop\":{\"a\":1}}")));
}
And some other mangling is less obvious:
@Test public void put_strips_quotes_from_strings_sometimes() {
JSONObject obj = new JSONObject();
String value = "\"[value]\""; // also "\"{value}\""
obj.put("prop", value);
assertThat(obj.get("prop"), is(equalTo(value.replaceAll("\"", ""))));
assertThat(obj.toString(), is(equalTo("{\"prop\":{\"a\":1}}")));
assertThat(obj.toString(), is(equalTo("{\"prop\":\"[value]\"}")));
}
Do you want this property value to be a scalar or array?
@Test public void accumulate_one_scalar() {
JSONObject obj = new JSONObject();
obj.accumulate("a", 1);
assertThat(obj.toString(), is(equalTo("{\"a\":1}")));
}
@Test public void accumulate_two_scalars() {
JSONObject obj = new JSONObject();
obj.accumulate("a", 1);
obj.accumulate("a", 2);
assertThat(obj.toString(), is(equalTo("{\"a\":[1,2]}")));
}
@Test public void accumulate_one_array() {
JSONObject obj = new JSONObject();
obj.accumulate("a", ImmutableList.of(1, 2));
assertThat(obj.toString(), is(equalTo("{\"a\":[1,2]}")));
}
@Test public void accumulate_two_arrays() {
JSONObject obj = new JSONObject();
obj.accumulate("a", ImmutableList.of(1, 2));
obj.accumulate("a", ImmutableList.of(3, 4));
assertThat(obj.toString(), is(equalTo("{\"a\":[1,2,[3,4]]}")));
}
("fixed" in the org.json version, but incompatibly)
The central serialization method
public String toString() {
if( isNullObject() ){
return JSONNull.getInstance()
.toString();
}
try{
Iterator keys = keys();
StringBuffer sb = new StringBuffer( "{" );
while( keys.hasNext() ){
if( sb.length() > 1 ){
sb.append( ',' );
}
Object o = keys.next();
sb.append( JSONUtils.quote( o.toString() ) );
sb.append( ':' );
sb.append( JSONUtils.valueToString( this.properties.get( o ) ) );
}
sb.append( '}' );
return sb.toString();
}catch( Exception e ){
return null;
}
}
Lots of tests were written to get hold of a produced JSONObject
and examine what was inside it. Refactoring these to format the object out to JSON encourages testing against an interface rather than an implementation.
/* old style */ @Test public String the_thing_has_a_price() {
JSONObject obj = renderer.render(theThing());
assertEquals(obj.getJSONObject("price").get("currency"), "GBP");
assertEquals(obj.getJSONObject("price").get("amount"), 1.2);
}
/* new style */ @Test public String the_thing_has_a_price() {
JSONObject obj = renderer.render(theThing());
// allow single quotes in reference to avoid backslash-quote overload
// btw JSONObject always uses relaxed parsing
assertThat(obj.toString(),
isEquivalentTo("{ 'price': { 'currency' : 'GBP',"
+ " 'amount': 1.2 }"));
}
Structural matchers if an equivalence test is too detailed.
@Test public String the_thing_has_a_price() {
JSONObject obj = renderer.render(theThing());
assertThat(obj,
structuredAs(jsonObject()
.withProperty("price",
jsonObject()
.withProperty("currency", "GBP")
.withProperty("amount", jsonNumber(closeTo(1.2, 0.01))))));
}
In fact, these matchers are implemented using the Jackson tree model, so this is almost an interop check.
We are now using just two JSON librarys: the org.json generation of json-lib and Jackson.