A Tour of the API

Borislav Iordanov edited this page Aug 30, 2014 · 10 revisions

The APIs is really concise to use, but sometimes conciseness needs a bit of upfront explanation, or at least some initial pointers, so here goes. Javadocs are here: http://bolerio.github.io/mjson/apidocs/index.html.

Basics

Every type of entity is represented as a Json instance. Internal representations are private, so you only work with that class.

Every method that is a mutating operation on the Json instance returns the instance itself so that method chaining is possible.

Creating Json Entities

To create a Json entity, you need a value. The types of values supported by default are: null, java.lang.Boolean, java.lang.Number, java.lang.String, java.util.Collection, java.util.Map and Java arrays. If you have any of those types of values you can call

Object anything = ...
Json j = Json.make(anything);

to convert it to a Json object. Naturally, collections and arrays translate to JSON arrays while maps translate to JSON objects. Consequently, members of the collection or array that you are trying to convert must be convertible as well and maps' keys must by of type string since they are to become property names. Having recognizable types is the only constraint. Otherwise you can arbitrarily nest collections within maps within array and what not.

Converting Java values to Json entities is done by an instance of Json.Factory. The paragraph above states what the default such factory supports. In particular, the default factory does not support Java beans. If you want that, you have to use a custom factory as explain later.

For any of the primitive types, you have to use the Json.make method. For the null entity, you can call Json.nil() which will give the singleton JSON null representation. You can also construct empty objects and array and populate them with API calls, e.g.:

// create an array and add 2 elements to it
Json A = Json.array().add("first element").add("second element");
// another way to create the same array using varargs
A = Json.array("first element", "second element");

// create an object and set two properties to it
Json O = Json.object().set("item", "Kindle").set("price", 79);
// another way to create the same object using varargs
// note that this method expects an even number of arguments with each 
// even-numbered position being a string, the property name, followed
// by value
O = Json.object("item", "Kindle", "price", 79);

Finally, let's not forget that you can create a Json by parsing a string:

Json j = Json.read("[4,5,{}, true]"); // j.at(1) == 5;

Examining Json Entities

When you have a Json entity, you can obtain its value by calling its getValue() method which will return the underlying Java object.

To find out what type a given Json is, use one of the "is" methods: isNull(), isNumber(), isBoolean(), isString(), isObject(), isArray(). Once you've made sure that a Json is of the type you are expecting, you can use one of the "as" methods to obtain an appropriately typed Java value: asInteger(), asDouble(), asBoolean(), asString(), asJsonMap(), asJsonList() etc. For example:

if (j.isString())
   System.out.println("Prefix: " + j.asString().substring(0,5));

Determining the type is also important for arrays and objects to know which of the mutating method will be available:

if (j.isArray())
{
   j.add("foo");  // will work
   j.set("bla", "foo"); // will throw an UnsupportedOperationException because set is only for objects
}

Examining the elements of arrays and objects is done with the at method which is overloaded to take either an array index or a property name:

A.at(3); // get the 4th element of an array
O.at("customer"); // get the 'customer' property of an object
A.at(2).at("name").asString(); // get the name as a Java string of the 3d element of an array

If an array index is out of bounds, you'd simply get an IndexOutOfBounds exception. If a property doesn't exist, you'd get a Java null reference value (not a Json.nil). Therefore, a null reliably indicates the absence of a property while a Json.nil represents the JSON null value. There is a convenient variant of the object at method that lets you set a property with a default value:

O.at("customer", Json.object()); // get the customer property, setting it to the empty object if it doesn't exist

As another example, say you want to return the name property of an object that may very well not have a name. You want to return the empty string if the object doesn't have a name property. The usual logic would be:

return O.has("name") ? O.at("name").asString() : "";

But you could also say:

return O.at("name", "").asString();

Note that this variant of at changes the object. It doesn't simply return a default value on a missing property, it actually sets that default value. Yes, that's a bit controversial as a design choice, but that's what it is.

If you want to traverse all elements of an array, you can obtain a view (a clone of the underlying object) with the asList() method. To avoid cloning and access the underlying implementation itself, use asJsonList() instead. Similarly for objects, asMap() will clone and return the underlying map while asJsonMap() will return it without cloning. Consequently manipulating the structures returned by asJsonList() or asJsonMap() will be reflected in the enclosing Json instances. If the entity is accessed from multiple threads and potentially modified, then cloning is advised to avoid the dreaded ConcurrentModificationException.

Arrays and objects have some extra methods to examine their members. You can test the value of an array element or an object property with the is(...) method:

// check if the 4th element of the Json array A is the boolean value true
if (A.is(4, true))
{
...
}
// check if the "customer" property of the Json object O is the string value "Pedro"
if (O.is("customer", "Pedro"))
{
}

Finally, you can check for the presence of a property in a Json object with the has method:

if (O.has("customer")) // does the Json object O contain a property name "customer"?
{
...
}

Navigating Json Structures

Navigating is done with Json.at method, which is overloaded for array and objects:

// get the 11th integer of a Json array
A.at(10).asInteger(); 
// get the first elements of the list of items of 
// the order property of a Json object
O.at("order").at("items").at(0); 

For convenience, each Json entity that is a member of an array or the property of an object holds a reference to said array or object. That reference is accessible with the Json.up method. So for example we have that:

if (O == O.at("order").at("items").at(0).up().up().up())
   // this is true!

Modifying Json Structures

Modifying a primitive Json is not permitted: booleans, numbers and string are immutable.

Modifying an object is done mainly with the set and delAt, where set serves both an add and update function. And we already saw the version of at that creates a default property. The set method sets a property, adding a new one or overwriting any previous value. Unsurprisingly, delAt removes a property:

O.set("name", "Ronaldo").set("address", Json.object("street", "22 Acacia avenue")); // set a bunch of properties
O.delAt("address").set("name", "Ronaldhinio"); // remove the address property and change the name

Sometimes it's useful to delete a property and do something with the value afterwards. For that, there is the method atDel which first obtains the property value and then deletes it:

O.atDel("address").at("street"); // remove the address and get the street portion from it

For arrays, there is a slightly different set of methods: add to append to the array and remove to remove an element by value. You can also use with delAt and atDel with an integer argument to delete elements at a particular index rather than by value. Some example:

Json A = Json.array(1,2,3,4,5);
A.delAt(3).equals(Json.array(1,2,3,5)); // returns true
A.atDel(2).equals(3); // returns true
A.add(6).remove(1).equals(Json.array(2,5,6)); // returns true

Finally, we have two extra utility methods: with and dup. The with method will merge one object into another or append an array to another array. The dup method will clone any Json entity:

Json O1 = Json.object("name", "Tom", "age", 35);

// let's clone that object
Json aclone = O1.dup();
aclone != O1 && aclone.equals(O1); // returns true

Json O2 = Json.object("family", "Brown", "address", "22 Acacia avenue, Middle Earth");
// merge O2 into O1 to obtain
// {"name" : "Tom", "age": 35,"family" : "Brown", "address" : "22 Acacia avenue, Middle Earth"}
// to avoid modify O1, we first clone it
O1.dup().with(O2); 

Json A1 = Json.array(1,2,3);
Json A2 = Json.array(3,2,1);
// merge A2 to A1, so A1 becomes [1,2,3,3,2,1]
// here A1 is modified since we didn't clone it before the append
A1.with(A2);

Parent-Child Relationships

Json arrays and objects contain other JSON elements. One of the goal of this library was to support quick and easy navigation within a structure. Hence we provide a Json.up method to jump from an element of an array or a property value up to the enclosing array or object. This means, we store a reference to the enclosing entity in every Json. Consequently, every Json can only belong to one parent so that the 'up' method knows what to return. This is problematic since it is not uncommon for containers to share members. The API currently changes the parent of an entity every time you set that entity as an object property or add it to an array. Thus if you have:

Json hector = Json.object("name", "Hector", "age", 12);
Json mila = Json.object("name", "Mila", "age", 11);
Json team_1 = Json.array(hector, mila);
Json children = Json.object().set("son", "hector");
// Here hector has changed its enclosing Json to the 'children' while mila remains with the
// same enclosing team_1.
hector.up() == children && mila.up() == team_1; // true

// yet, hector is still a member of team_1
team_1.at(0) == hector; // true

So the 'up' method will return the last structure to which an entity was added.

This is an API deficiency so far. There are several ways to go about it, but whatever choice is made, it should be something that an application can decide. So it should be a configuration option. Call it a design shortcoming.

Customization

You can customize the implementation of primitives for example to provide mutability. It's hard to imagine a use case for this. Or you could customize the implementation of the string primitive to make the equals comparison case insensitive - this is an easier use case to imagine.

You could customize the implementation of objects or arrays for optimization purposes for example, or to provide some synchronization for multi-threaded applications (i.e. use a ConcurrentHashMap instead of a plain HashMap).

Or you could customize the support Java types in the Json.make factory method. For example, you may want to implement mapping of JSON<->Java beans like a lot of JSON libraries do.

All the above mentioned customizations are possible by implementing the Json.Factory interface. This interface determines entirely the internal representation of the JSON entities and how they are converted to/from Java. The conversion from Java to a JSON is done by the Json.make method which calls the currently configured factory implementation. The conversion from JSON to Java is what the Json.getValue() method is supposed to do. So for example, the default getValue for an object will return a Java map. But you could provide an implementation that returns a Java bean (or something else) being represented. Essentially, anything of interest that you'd want to customize, you can do with that factory interface. The default implementation is also publicly exposed so that you can choose to customize only certain parts of it (see Json.DefaultFactory).

Now, I mentioned the currently configured factory. Here is what current means in this case: if there is a factory configured for the current thread, use that factory, otherwise use the global factory. So we have a global factory in static variable and thread local factory. Those two should provide enough flexibility if you have different use cases or if you have to deal with libraries that have incompatible requirements when it comes to JSON handling.

To set the global factory, use Json.setGlobalFactory and to set the thread local factory, use the Json.attachFactory factory. To reset the thread local factory to null, use the Json.dettachFactory method (that could go in a finally block).

Validating With JSON Schema

You can write JSON schemas and use mJson to validate JSON documents with them. The schemas can be constructed as Json objects or by stored externally and accessible over the internet. Here is a quick example:

// A simple schema that accepts only JSON objects with a mandatory property 'id'.
Json.Schema schema = Json.schema(Json.object("type", "object", "required", Json.array("id")));
schema.validate(Json.object("id", 666, "name", "Britlan")); // true
schema.validate(Json.object("ID", 666, "name", "Britlan")); // false

For more see JSON Schema Support