Thursday, January 30, 2014

Making your JSON-P (JSR-353) code slightly prettier

The JSON-P API as described in JSR-353 is a limited API for working with JSON; but at the basic level it will do the job. It is possible to add a few utility methods that can make your code, in my eyes, prettier.

The first think that annoyed me was the use of Json.createObjectBuilder() and Json.createArrayBuilder() when trying to construct a JSONObject.

So lets create a nice helper class with some very short method names for both:

public static JsonObjectBuilder ob() {
  return Json.createObjectBuilder();
}

public static JsonArrayBuilder ab() {
  return Json.createArrayBuilder();
}

This makes the creation of object just that little bit less wordy:

JsonObject object = ob()
  .add("hello", "world")
  .add("fark", ".com")
  .add("fish", ob()
    .add("child", "child"))
  .add("array", ab()
    .add("one")
    .add("two")
    .add(ob()
      .add("boolean",true)))
  .build();

The second problem is accessing element in a JsonObject can be wordy; but it is relatively easy to knock up a method that would allow some simply XPath like accessor:

public static <T extends JsonValue> T get(JsonStructure structure, 
  String path, Class<T> type) {

  String segments[] = path.split("/");
  JsonValue currentValue = structure;
  for (String segment : segments) {

    if (segment.length() == 0) {
      continue;
    }

    if (currentValue instanceof JsonObject) {
      JsonObject currentObject = (JsonObject) currentValue;
      currentValue = currentObject.get(segment);
    } else if (currentValue instanceof JsonArray) {
      if (segment.startsWith("[") && segment.endsWith("]")) {
        int index = Integer.parseInt(segment.substring(1, segment.length() - 1));
        currentValue = ((JsonArray) currentValue).get(index);
      } else {
        throw new IllegalArgumentException("Array type requires key of the form [n]");
      }
    } else {
      throw new IllegalStateException("Value types are not decomposible" 
        + currentValue.getValueType());
    }

  }

  return type.cast(currentValue);

}


// Example to get hold of a string value

System.out.println(get(object, "/fish/child", JsonString.class));


Of course even this is a little bit untidy as you have to care about the internal JsonValue types which is a pain; and deal with possibly null pointers without the aid of Optional. It doesn't take much to wrap these up though.

public static String getString(JsonStructure structure, String path) {
  JsonString value = get(structure, path, JsonString.class);
  return value != null ? value.getString() : null;
}

public static boolean is(JsonStructure structure, String path) {
  JsonValue value = get(structure, path, JsonValue.class);
  return value != null ? value == JsonValue.TRUE : false;
}

public static boolean isNull(JsonStructure structure, String path) {
  JsonValue value = get(structure, path, JsonValue.class);
  return value != null ? value == JsonValue.NULL : false;
}

public static BigInteger getBigDecimal(JsonStructure structure, String path) {
  JsonNumber value = get(structure, path, JsonNumber.class);
  return value != null ? value.bigIntegerValue() : null;
}

public static int getInt(JsonStructure structure, String path) {
  JsonNumber value = get(structure, path, JsonNumber.class);
  return value != null ? value.intValue() : null;
}

public static JsonArray getArray(JsonStructure structure, String path) {
  return get(structure, path, JsonArray.class);
}

public static JsonObject getObject(JsonStructure structure, String path) {
  return get(structure, path, JsonObject.class);
}

This means you can write more direct code such as:

if (is(object, "/array/[2]/boolean")) {
  System.out.println(getString(object, "/fish/child"));
}

Sometimes it only take a few statically imported methods to make a API more useful / easier to read.

1 comment:

Marcel Overdijk said...

Nice article.
I've the same feeling about JSR-353. The basic stuff is there but you need custom helpers to make life easier.

My biggest paint point is that adding key/values to JsonObject throws NPE when value is null. This means a lot of of/else boilerplate before adding key/value pairs.
I've ended up using some utility class as well.