Sunday, June 5, 2011

JSON Library

[NOTE: The links below point to the latest release, as described in this new blog entry.]

JSON (JavaScript Object Notation) is a lightweight data-interchange format. You knew that already. If not, continue reading on http://www.json.org.

It's supposed to be about simplicity and clarity. Something minimal, intuitive, direct. Yet, I couldn't find a Java library to work with it in this way. The GSON project is pretty solid and comprehensive, but while working with REST services and coding some JavaScript with JSON in between, I got frustrated of having to be so verbose on the server-side while on the client-side manipulating those JSON structures is so easy. Yes, JSON is naturally embedded in JavaScript, so syntactically it could never be as easy in a Java context, but it just didn't make sense all that strong typing of every JSON element when the structures are dynamic and untyped to being with. It seemed like suffering the verbosity of strong typing without getting any of the benefits. Especially since we don't map JSON to Java or anything of the sort. Our use of JSON is pure and simple: structured data that both client and server can work with. 

After a lot of hesitation and looking over all Java/JSON I could find (well, mostly I examined all the libraries listed on json.org), I wrote yet another Java JSON library. Because it's rather independent from the rest of the project, I separated it. And because it has a chance of meeting other programmers' tastes, I decided to publish it. First, here are the links:

Documentation: http://www.sharegov.org/mjson/doc

Download: http://www.sharegov.org/mjson/mjson.jar

Source code: http://www.sharegov.org/mjson/Json.java 

The library is called mjson for "minimal JSON". The source code is a single Java file (also included in the jar). Some of it was ripped off from other projects and credit and licensing notices are included in the appropriate places. The license is Apache 2.0.

The goal of this library is to offer a simple API to work with JSON structures, directly, minimizing the burdens of Java's static typing and minimizing the programmer's typing (pun intended). 

To do that, we emulate dynamic typing by unifying all the different JSON entities into a single type called Json. Different kinds of Json entities (primitives, arrays, objects, null) are implemented as sub-classes (privately nested) of Json, but they all share the exact same set of declared operations and to the outside world, there's only one type. Most mutating operations return this which allows for a method chaining. Constructing the correct concrete entities is done by factory methods, one of them called make which is a "do it all" constructor that takes any Java object and converts it into a Json. Warning: only primitives, arrays, collections and maps are supported here. As I said, we are dealing with pure JSON, we are not handling Java bean mappings and the likes. Such functionality could be added, of course, but....given enough demand.

As a result of this strategy, coding involves no type casts, much fewer intermediary variables, much simpler navigation through a JSON structure, no new operator every time you want to add an element to a structure, no dealing with a multitude of concrete types. Overall, it makes life easier in the current era of JSON-based REST services, when implemented in Java that is.

In a sense, we are flipping the argument from the blog Dynamic Languages Are Static Languages and making use of the universal type idea in a static language. Java already has a universal type called Object, but it doesn't have many useful operations. Because the number of possible JSON concrete types is small and well-defined, taking the union of all their interfaces works well here. Whenever an operation doesn't make sense, it will throw an UnsupportedOperationException. But this is fine. We are dynamic, we can guarantee we are calling the right operation for the right concrete type. Otherwise, the tests would fail!

Here's a quick example:

import mjson.Json;

Json x = Json.object().set("name", "mjson")
.set("version", "1.0")
.set("cost", 0.0)
.set("alias", Json.array("json", "minimal json"));
x.at("name").asString(); // return mjson as a Java String
x.at("alias").at(1); // returns "minimal json" as a Json instance
x.at("alias").up().at("cost").asDouble(); // returns 0.0

String s = x.toString(); // get string representation

x.equals(Json.read(s)); // parse back and compare => true

For more, read the documentation at the link above. No point in repeating it here.

This is version 1.0 and suggestions for further enhancements are welcome. Besides some simple nice-to-haves, such as pretty printing or the ability to stream to an OutputStream, Java bean mappings might turn out to be a necessity for some use cases. Also, jQuery style selectors and a richer set of manipulation operations. Closures in JDK 7 would certainly open interesting API possibilities. For now, we are keeping it simple. The main use case is if you don't have a Java object model for the structured data you want to work with, you don't want such a model, or you don't want it to be mapped exactly and faithfully as a JSON structure.

Cheers,

Boris

13 comments:

  1. I was looking for some library like this, I think it express in a natural manner the JavaScript Object Notation. I was implementing my own classes, for JsonString and JsonArray, but I'm thinking on change them for the ones in <>.

    public void getJsonData() {
    JsonString json = new JsonString();
    try {
    JsonArray key = new JsonArray();
    JsonArray description = new JsonArray();

    Iterator iterator = service.iterator();
    while( iterator.hasNext() ) {
    EntityA obj = iterator.next();
    key.addInteger(obj.getKey());
    description.addString(obj.getValue());
    }
    json.add("key", key);
    json.add("description", description);
    json.add("result", Constants.SUCCESS);
    }catch( Exception ex ) {
    json.add("result", Constants.ERROR);
    json.add("error", ex.getMessage());
    } finally {
    WebUtil.setJsonResponse(response, json.toString());
    }
    }

    ReplyDelete
  2. Thanks, please give it a try. It's certainly more concise that the libraries out there. I hope it has enough features to cover your needs.

    Best,
    Boris

    ReplyDelete
  3. Have you tested the little example in the javadoc?
    The "menu" layer is missing and "position" is the root layer instead of the "menu" layer.

    ReplyDelete
  4. I think it is missing a final ".up()" :)

    ReplyDelete
  5. @Lee: you're right, thanks for pointing that out!

    Best,
    Boris

    ReplyDelete
  6. So far mjson looks nice, but there is a stupid bug - JsonNumber returns false for isNumber() :-)

    Fixed by returning true on line 798.

    Do you use a source control for this project? Do you have any unit tests?

    ReplyDelete
  7. Thanks Nir. It is under the sharegov source control, which hosts several big projects and currently has somewhat restricted access even though the code is open-source licensed. We might separate mjson in one of the public repositories like bitbucket. In the meantime, I will incorporate fixes like yours as they come up and make an updated release.

    ReplyDelete
  8. Boris,

    A few improvements and suggestions.

    1. I changed your use of HashMap to LinkedHashMap in class ObjectJson 1st line and in its asMap() method so that the map entries come out in the same order as they were put in. (A cosmetic improvement, I admit, but much easier to debug complex jsons). A tiny cost is incurred to maintain the links under the covers, but I felt it was worth it.

    2. Also, I like to keep my "unchecked" and "rawtypes" warnings on and then add @SuppressWarnings when I feel confident that I'm still doing the right thing. I found 5 places where I had to add them. You should be able to find them easily enough if you wish to annotate these (or I can send you a list). I also had to suppress "unused" for the entire class, but considering how this class is constructed I am not surprised (or concerned). A little more worrysome, however, is that I had to suppress "hiding" on the read(CharacterIterator it) method. Although the compiler is able to do the right thing, you may want to rename that parameter for code clarity.

    3. And, where I see simple spelling errors, extra long lines or other issues in the javadoc comments I have been cleaning those up as I go. This screws up the line numbers, unfortunately, so your comment above about "returning true on line 798" made my subsequent self-repair more challenging :)

    I'd be glad to send you these fixes if you wish.

    Lee.

    ReplyDelete
  9. Hi Lee,
    Yes,I am fine with those. Please pass them on and I'll incorporate them into the next version.

    Thanks!
    Boris

    ReplyDelete
  10. Nice work.

    If you don't have the time to update your code right now, I can push it to github so people can collaborate there.

    ReplyDelete
  11. HI Aaron,

    Thanks. I was actually about to make an update. Kind of hesitating to grow the API too much, but one particular feature I needed was the ability to provide one's own implementation of the Json types, hence a new interface:

    public class Json
    {
    public static interface Factory
    {
    Json nil();
    Json bool(boolean value);
    Json string(String value);
    Json number(Number value);
    Json object();
    Json array();
    Json make(Object anything);
    }

    public static class DefaultFactory implements Factory
    {
    ... default implementation that can be extended to overwrite only some parts..

    etc.
    }

    This also allows the plugin of custom mapping b/w arbitrary Java objects and Json via an implementation of the 'make' method. Should be easy for example to implement a make method that handles Java beans with introspection, which is something that I don't want by default as part of the API.

    I've also added another top-level method: is(String, Object) for objects and is(int, Object) for arrays. Those methods return true if the given named property (or indexed element) is equal to the passed in Object. For example is(name, value) is equivalent to 'has(name) && at(name).equals(make(value))'.

    Would appreciate an opinion from anyone following/using this code.

    Best,
    Boris

    ReplyDelete
  12. Hi,

    Ok, I've made the update to 1.1 version with all the above changes. There's also a 'dup' method that I forgot to mention that makes a copy of a Json object. The old version 1.0 is still available: just replace 'mjson' by 'mjson1.0' in the links from the blog post. I'll probably make a separate blog post describing what's in the new version.

    Cheers,
    Boris

    ReplyDelete