The Value of JSON Values
A deep dive into JSON.
Join the DZone community and get the full member experience.Join For Free
This article introduces json-values, a pure functional Java library to work with JSON. In this first article, we'll see some impressive recursive data structures to model essential concepts in software development like data validation, data generation, and parsers.
The first and most important virtue of json-values is that JSON objects are immutable and implemented with persistent data structures, better known in FP jargon as values. As Pat Hellan said, immutability changes everything.
It's a fact that, when possible, working with values leads to code with fewer bugs, is more readable, and is easier to maintain. Item 17 of Effective Java states that we must minimize mutability. Still, sometimes it's at the cost of losing performance because the copy-on-write approach is very inefficient for significant data structures. Here is where persistent data structures come into play.
Most functional languages, like Haskell, Clojure, and Scala, implement persistent data structures natively. Java doesn't. The best alternative I've found in the JVM ecosystem is the persistent collections provided by the library vavr. It provides a well-designed API and has a good performance.
The standard Java programmer finds it strange to work without objects and all the machinery of frameworks and annotations. FP is all about functions and values; that's it. I will try to cast some light on how we can manipulate JSON with json-values following a purely functional approach. First things first, let's create a JSON object:
As you can see, its definition is like raw JSON. It’s a recursive data structure. You can nest as many JSON objects as you want. Think of any imaginable JSON, and you can write it in no time.
But what about validating JSON? We can define the JSON schema following precisely the same approach:
I’d argue that it is very expressive, concise, and straightforward. I call it
json-spec. I named it after a Clojure library named spec. Writing specs feels like writing JSON. Strict specs don't allow keys that are not specified, whereas lenient ones do. The real power is that you can create specs from predicates and compose them:
As you can see, the spec's structure remains the same, and it’s child’s play to define optional and nullable fields.
Another exciting thing we can do with specs is parsing strings or bytes. Instead of parsing the whole JSON and then validating it, we can verify the JSON schema while parsing it and stop the process as soon as an error happens. After all, failing fast is important as well!
I've tried different alternatives:
- Jackson + bean validation annotations (hibernate-validator implementation)
The results are as follows:
Another critical aspect of software development is data generation. It’s an essential aspect of property-based testing, a technique for the random testing of program properties very well known in FP. Computers are way better than humans at generating random data. You'll catch more bugs testing your code against a lot of inputs instead of just one. Writing generators, like specs, is as simple as writing JSON:
Consider the following method,
It shows the essence of property-based testing, even if it's far from being a real implementation like Quickcheck or ScalaCheck. You pass in a generator and a predicate representing a property that your code has to satisfy. Then, the predicate is tested against randomized input data produced by the generator. You have to indicate the number of iterations; otherwise, it never ends!
Given the previous JSON generator, imagine we need to generate addresses with either all the fields (name, number, and city) or none of them. We can use brute force to save us a lot of time using the
suchThat combinator that returns a new generator that produces values that satisfy the given predicate:
Care is needed to ensure there is a high chance the generator will satisfy the predicate. By default,
suchThat will try 100 times to generate a value that satisfies the predicate. If no value passes this predicate after this number of iterations, a runtime exception is thrown. I love this feature. Most of the time, the predicate you passed in is an existing one that is used for validation purposes.
Data generation and validation are critical in software. Generating and validating your data with such concise and readable data structures has a significant impact on productivity and maintainability.
Let's go over the most important functions to manipulate JSON that you can find in json-values. Consider the following JSON:
It can be modeled as a set of path-value pairs:
As you may notice,
* represents all the paths not defined for that JSON, and
JsNothing is their associated value. This model is convenient for expressing two of the most critical functions:
get method always returns a value, no matter what path is passed in (if the path doesn't exist it returns the singleton of
JsNothing). It's a total function. Functional programmers strive for total functions. Their signature still reflects reality: no exceptions, and it never returns null. Following the same philosophy, if you set a value at a specific path, it will always be created. In the next line of code after setting that value, it will be at the specified path. The following property still holds:
What do you think setting
JsNothing at a path does? Well, it has to remove the value, so that
FP has to do with honesty. Establishing laws makes it easier to reason about the code we write. By the way, the
set method always returns brand new JSON; if you remember well, json-values uses persistent data structures.
There are times when it's more convenient to use the following functions to get some data out:
Let's introduce the jewels in the crown of FP:
Consider the following operation. It maps a JSON recursively, trimming only the elements that are strings:
We need to check the type and make a conversion. It's not very declarative, is it? Prisms come to the rescue. If you think of what a Prism does to light, it happens the same with the sum-type JsValue. We have several subtypes to consider (int, long, string, obj, array, instant, bool), and we want to focus on a specific one. You can refactor the previous code into:
Every type in json-values has a Prism. Another example with instants:
Prisms are a specific type of optics. I'll cover optics in the next article. Nevertheless, I'd like just to point out some critical aspects of them. It’s ubiquitous to navigate through recursive data structures like JSON objects and arrays to find, insert, and modify data. It’s a cumbersome and error-prone task (NullPointerException is always lurking around), requiring a defensive programming style with much boilerplate code. The more nested the structure is, the worse. FP uses optics to cope with these limitations.
Some other handy methods are union and intersection:
Even if you are using the standard Java libraries to work with JSONs and a pure functional library doesn't fit in your project, you can benefit from json-values to generate and validate Json in your tests.
There are some related projects you may find interesting
- mongo-values: to work with the MongoDB Java driver and json-values without any kind of conversion to/from BSON.
- vertx-effect: replaces the Vert.x JSON with json-values. Persistent data structures especially shine in architectures based on the actor model.
- vertx-mongodb-effect: built on top of vertx-effect and with mongo-values
- json-scala-values: the Scala version of json-values
- json-kotlin-values: currently under development.
- json-values provides a persistent JSON with a simple, declarative, and efficient API.
- We've seen different recursive data structures to model JSON objects, specs, and generators. You can open a JShell and start writing and testing them right away. It should be easy to interact with the code we develop. A unit test is not a proper way of interacting with the software you write. You and your code will end up growing apart.
Opinions expressed by DZone contributors are their own.