Nereides - object oriented JSON library wrappers

Ship, 1905 - Mikalojus Konstantinas Čiurlionis

You are happily coding. You are in the flow. You have a cup of coffee next to you, and object oriented code flows freely. Suddenly, you have a String of JSON data, and you need the value of a certain field inside. Oh no. You realize you now have to scroll all the way to the top of the class where your fields are declared and create a new ObjectMapper, then scroll all the way down to the place you were typing and choose one of the hundred readValue or readTree (which one means what again?) methods. Your flow is broken.

Json

How great would it be if you could just declare a JSON object based on your data right here and now and start using it without any need of mappers, factories and other imperative noise. Fortunately for you, there is a family of libraries specifically for this - they are called Nereides. With Nereid for Jackson, you can do this:

Json json = new Json.Of(string)

Nereides are object oriented wrappers for popular JSON processing libraries like jackson-databind and javax.json. They are designed for ease of use and high extensibility via the Decorator pattern, which is used liberally in Elegant Objects programming style. Since the core interface Json has only one method, it is very easy to create new implementations. Each new object just needs to know how to represent itself as a stream of bytes, which, by the way, usually doesn’t need to be implemented by the developer and can be delegated to encapsulated JSONs instead. I will show how to implement custom domain related JSONs later in this post, but first let’s look at a couple of extensions that come out-of-the-box with Nereides.

Smart Json

SmartJson is a Json which can represent itself as various other data types. We just need to give it any other Json, and we can use it like this:

// Convert it to String:
String textual = new SmartJson(json).textual();

// Convert it to pretty formatted String:
String pretty = new SmartJson(json).pretty();

// Convert it to byte array:
byte[] bytes = new SmartJson(json).byteArray();

It also knows how to reach its own inner structures.

// Get a String field value:
Optional<String> leaf = new SmartJson(json).leaf("nymph");

// Get a deeply nested Json:
SmartJson nested = new SmartJson(json).at("/path/to/nested/json");

SmartJson is implemented in Yegor’s Smart Object pattern. Since we want to have only one method in Json interface, but we need a lot of methods to be able to turn it into different datatypes such as String and byte array, we wrap it into another object which implements Json interface, but also has all the other methods we need. It’s still a Json, but it’s also smart.

Mutable Json

Another useful extension in the library is MutableJson which can be used to either add elements to existing JSONs, or to build them from scratch. The intended way of using Nereides is to define custom objects for JSONs with more or less fixed structure in your domain (more about that later), but if you need to quickly build a new JSON by hand, you can use MutableJson. Say, you need this JSON:

{
  "ocean" : {
    "nereid1" : {
      "name" : "Thetis",
      "hair" : "black"
    },
    "nereid2" : {
      "name" : "Actaea",
      "hair" : "blonde"
    },
    "stormy" : true
  }
}

You can build it like this:

new MutableJson().with(
    "ocean",
    new MutableJson().with(
        "nereid1",
        new MutableJson()
            .with("name", "Thetis")
            .with("hair", "black")
    ).with(
        "nereid2",
        new MutableJson()
            .with("name", "Actaea")
            .with("hair", "blonde")
    )
    .with("stormy", true)
)

Compare with the work you would need to do if you tried to build it with one of the more widely used libraries.


ObjectMapper mapper = new ObjectMapper();
ObjectNode ocean = mapper.createObjectNode();
ocean.set(
    "nereid1",
    mapper.createObjectNode()
        .put("name", "Thetis")
        .put("hair", "black")
);
ocean.set(
    "nereid2",
    mapper.createObjectNode()
        .put("name", "Actea")
        .put("hair", "blonde")
);
ocean.put("stormy", true);
mapper.createObjectNode().set("ocean", ocean);

Notice how the first example is an immediate declaration of JSON in a single statement, while the second one is a step by step building procedure1 using imperative language like “put”, “set” and “create”.

Domain Jsons

By the way, do you know why the libraries are called Nereides? Since we are processing JSONs, it is worth to remember the adventurer Jason, who once sailed the ship Argo in search of the Golden Fleece. One day the Argonauts sailed right into the Wandering Rocks and almost crashed, but the sea nymphs called Nereides, the daughters of Nereus and Doris and companions of Poseidon, guided them safely through.

So let’s say our domain is Jason’s quest. We have a JSON datastore2 we will call Argo (after the ship) and we will store our domain objects, the Argonauts, there.

public interface Argo extends JsonStore {
    void onboard(Json argonaut);
    Json find(UUID id);
}

We could create an Argonaut and onboard him like this:

Json asterion = new MutableJson()
    .with("id", UUID.randomUUID().toString())
    .with("name", "Asterion")
    .with(
        "primary-weapon",
        new MutableJson()
            .with("type", "Melee")
            .with("name", "Iron Sword")
    )
    .with(
        "secondary-weapon",
        new MutableJson()
            .with("type", "Range")
            .with("name", "Yew bow")
    );
argo.onboard(asterion);

This is cumbersome (although not as much as with other libraries), because we would need to repeat most of this whenever we want to create each Argonaut. We could write some static helper methods to remove the duplication, but this is really not the way object oriented programming is done. Instead, we can create a domain class Argonaut.

final class Argonaut implements Json {
    private final Json body;

    public Argonaut(String name, Weapon weapon1, Weapon weapon2) {
        this.body = new MutableJson()
            .with("id", UUID.randomUUID().toString())
            .with("name", name)
            .with("primary-weapon", weapon1)
            .with("secondary-weapon", weapon2);
    }

    @Override
    public InputStream bytes() {
        return body.bytes();
    }
}

Notice a few things about it. It implements Json interface, and so can be directly stored in Argo. To implement Json, it needs to be able to represent itself as a stream of bytes, but there is no byte processing inside - the job is delegated to the Json it encapsulates, and that Json is built using MutableJson, so the developer doesn’t need to worry about byte processing at all. ID assignment is encapsulated inside, so the user doesn’t have to worry about that either. Also, weapons now have their own similar classes which implement Json as well. Since everything implements Json, there are really no conversions, no mapping and no boilerplate. We can create and onboard Argonauts as simply as this:

argo.onboard(
    new Argonaut("Asterion", new IronSword(), new YewBow())
);
argo.onboard(
    new Argonaut("Jason", new IronSpear(), new SilverDagger())
);

Conclusion

Nereides provide the ability to process JSONs in a truly object oriented way. There are no setters, no getters, no static methods, no nulls, no mappers, converters or factories. There is only one core interface, and it is extremely composable and extensible. Developers can easily implement their domain objects or JSON processing/building objects if they need different behaviour than SmartJson and MutableJson provide. Furthermore, using Nereides is fun! Try them out and give them a star.

  1. We cannot create this JSON in a single statement using this library at the time of writing this. 

  2. The implementation could be an adapter to MongoDB document collection, or whatever. 

Written on April 6, 2020