Type-safe JSON Parsing. Easy with DukeScript!
DukeScript is primarily optimized for designing client side responsive applications in Java and HTML, however libraries in its core are built with the goal of making Java/JavaScript interoperability easier. One of the essential concepts in JavaScript is JSON. Thus it is no surprise, DukeScript makes processing JSON from Java really smooth.
Where is My JSON?
First of all we need to obtain a JSON file. One of the free JSON services is the GitHub REST API, so let’s use it. The following command lists all repositories for a github account (actually the Jersey JAX-RS developers account):
$ curl https://api.github.com/users/jersey/repos
The ouput of this query has the following format. Save it into a file and let’s design a type-safe way to parse such a file with the help of DukeScript:
[
{
"id": 6109440,
"name": "hol-sse-websocket",
"full_name": "jersey/hol-sse-websocket",
"owner": {
"login": "jersey",
"id": 399710,
"avatar_url": "https://avatars.githubusercontent.com/u/399710?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/jersey",
"html_url": "https://github.com/jersey",
"followers_url": "https://api.github.com/users/jersey/followers",
"following_url": "https://api.github.com/users/jersey/following{/other_user}",
"gists_url": "https://api.github.com/users/jersey/gists{/gist_id}",
"starred_url": "https://api.github.com/users/jersey/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/jersey/subscriptions",
"organizations_url": "https://api.github.com/users/jersey/orgs",
"repos_url": "https://api.github.com/users/jersey/repos",
"events_url": "https://api.github.com/users/jersey/events{/privacy}",
"received_events_url": "https://api.github.com/users/jersey/received_events",
"type": "Organization",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/jersey/hol-sse-websocket",
"description": "Hands-on-lab on using server-sent events and web socket with Jersey and Tyrus.",
"fork": false,
"url": "https://api.github.com/repos/jersey/hol-sse-websocket",
"forks_url": "https://api.github.com/repos/jersey/hol-sse-websocket/forks",
"keys_url": "https://api.github.com/repos/jersey/hol-sse-websocket/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/jersey/hol-sse-websocket/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/jersey/hol-sse-websocket/teams",
"hooks_url": "https://api.github.com/repos/jersey/hol-sse-websocket/hooks",
"issue_events_url": "https://api.github.com/repos/jersey/hol-sse-websocket/issues/events{/number}",
"events_url": "https://api.github.com/repos/jersey/hol-sse-websocket/events",
"assignees_url": "https://api.github.com/repos/jersey/hol-sse-websocket/assignees{/user}",
"branches_url": "https://api.github.com/repos/jersey/hol-sse-websocket/branches{/branch}",
"tags_url": "https://api.github.com/repos/jersey/hol-sse-websocket/tags",
"blobs_url": "https://api.github.com/repos/jersey/hol-sse-websocket/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/jersey/hol-sse-websocket/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/jersey/hol-sse-websocket/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/jersey/hol-sse-websocket/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/jersey/hol-sse-websocket/statuses/{sha}",
"languages_url": "https://api.github.com/repos/jersey/hol-sse-websocket/languages",
"stargazers_url": "https://api.github.com/repos/jersey/hol-sse-websocket/stargazers",
"contributors_url": "https://api.github.com/repos/jersey/hol-sse-websocket/contributors",
"subscribers_url": "https://api.github.com/repos/jersey/hol-sse-websocket/subscribers",
"subscription_url": "https://api.github.com/repos/jersey/hol-sse-websocket/subscription",
"commits_url": "https://api.github.com/repos/jersey/hol-sse-websocket/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/jersey/hol-sse-websocket/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/jersey/hol-sse-websocket/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/jersey/hol-sse-websocket/issues/comments/{number}",
"contents_url": "https://api.github.com/repos/jersey/hol-sse-websocket/contents/{+path}",
"compare_url": "https://api.github.com/repos/jersey/hol-sse-websocket/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/jersey/hol-sse-websocket/merges",
"archive_url": "https://api.github.com/repos/jersey/hol-sse-websocket/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/jersey/hol-sse-websocket/downloads",
"issues_url": "https://api.github.com/repos/jersey/hol-sse-websocket/issues{/number}",
"pulls_url": "https://api.github.com/repos/jersey/hol-sse-websocket/pulls{/number}",
"milestones_url": "https://api.github.com/repos/jersey/hol-sse-websocket/milestones{/number}",
"notifications_url": "https://api.github.com/repos/jersey/hol-sse-websocket/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/jersey/hol-sse-websocket/labels{/name}",
"releases_url": "https://api.github.com/repos/jersey/hol-sse-websocket/releases{/id}",
"created_at": "2012-10-07T04:44:32Z",
"updated_at": "2014-06-29T22:29:42Z",
"pushed_at": "2013-05-29T16:56:03Z",
"git_url": "git://github.com/jersey/hol-sse-websocket.git",
"ssh_url": "git@github.com:jersey/hol-sse-websocket.git",
"clone_url": "https://github.com/jersey/hol-sse-websocket.git",
"svn_url": "https://github.com/jersey/hol-sse-websocket",
"homepage": null,
"size": 7750,
"stargazers_count": 11,
"watchers_count": 11,
"language": "Java",
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 5,
"mirror_url": null,
"open_issues_count": 1,
"forks": 5,
"open_issues": 1,
"watchers": 11,
"default_branch": "master"
},
{
"etc." : "etc."
}
]
Setting the Project Up
Let’s get started following the same way we start with regular HTML UI based DukeScript application. Follow the getting started tutorial if you are OK with using NetBeans. If you want to stay on command line, you can use:
$ mvn archetype:generate -DarchetypeGroupId=org.apidesign.html \
-DarchetypeArtifactId=knockout4j-archetype \
-DarchetypeVersion=1.1.2
Once our application is generated, we can start modifying the sample application to avoid displaying the UI, but rather parse our JSON file. Let’s remove:
src/main/webapp/pages/index.html
- no UI definitionssrc/main/java/your/pkg/DataModel.java
- no UI modelsrc/test
- we don’t need tests for this simple sample
The next step is to change dependencies in our pom.xml
- the sample project
can parse JSON inside of a running browser, but we want to do it in Java.
Luckily there is a JAR that can handle that. Just include it on runtime
classpath and it will handle all the parsing for us. Add the following dependency:
<dependency>
<groupId>org.netbeans.html</groupId>
<artifactId>ko-ws-tyrus</artifactId>
<version>${net.java.html.version}</version>
<scope>runtime</scope>
</dependency>
In addition to that let’s clean the Main.java
so it is ready for our parsing
code and looks like:
package your.pkg;
public final class Main {
private Main() {
}
public static void main(String... args) throws Exception {
}
}
Now we have an empty skeletal application what we can use to do the parsing, which can be verified by executing ‘mvn clean install’ - the project should built without any issues.
Parsing JSON Files
We are ready to start the parsing. There is the net.java.html.json.Models
class
in the core DukeScript API and it contains two overloaded variants of
the method parse
. One method can parse a single JSON object, the second can parse
a JSON array of multiple JSON objects.
Given the fact that the file we obtained from GitHub lists
an array of repositories, we should use the more complicated variant. Here is the
code:
public static void main(String... args) throws Exception {
BrwsrCtx ctx = parsingContext();
FileInputStream is = new FileInputStream("/tmp/jersey.json");
List<RepositoryInfo> arr = new ArrayList<>();
Models.parse(ctx, RepositoryInfo.class, is, arr);
System.err.println("all parsed data: " + arr);
RepositoryInfo first = arr.get(0);
System.err.println("id: " + first.getId());
is.close();
}
As part of the setup we get the parsing context. Then we open the JSON file
we want to parse. We allocate an array to hold the results and then ask the
parse
method to do the parsing and fill the list.
Then we print the whole array to output
and also access the first element in the array in a type-safe way and obtain
its id
property. Here is the output from the execution of the above program:
all parsed data: [{"id":6109440}, {"id":4368712}, {"id":14029306}, {"id":4522462}, {"id":911627}]
id: 6109440
Now one important question: How does the code know there is an id
property
in the JSON?
Defining the Model
DukeScript core APIs support a type-safe way of accessing JSON structures.
Moreover, in order to avoid writing tons of boiler-plate code,
we describe the JSON structure using annotations
and generate the rest with the help of annotation processors. As such the
RepositoryInfo
class is defined by:
@Model(className="RepositoryInfo", properties = {
@Property(name = "id", type = int.class)
})
public final class Main {
// now the main method
}
The above definition generates the RepositoryInfo
class with a single integer
property id
and code to unmarshall JSON into instances of that class. The
RepositoryInfo
class also defines a reasonable toString()
method - hence
the whole list of such objects can be dumped into output in JSON format.
What to do when you want to parse other JSON properties? Take a look at
the format of the downloaded JSON response file
and add a new property definition! For example there
is the name
property which seems to be of type String. Just change the definition
to:
@Model(className="RepositoryInfo", properties = {
@Property(name = "id", type = int.class),
@Property(name = "name", type = String.class),
})
Just by adding a single line, a getter getName()
is added to the
RepositoryInfo
info class and you can use it in Java code:
RepositoryInfo first = arr.get(0);
System.err.println("id: " + first.getId());
System.err.println("name: " + first.getName());
The output when running the modified program is now:
all parsed data: [{"id":6109440,"name":"hol-sse-websocket"}, {"id":4368712,"name":"jersey"}, {"id":14029306,"name":"jersey-1.x"}, {"id":4522462,"name":"jersey-1.x-old"}, {"id":911627,"name":"jersey-old"}]
id: 6109440
name: hol-sse-websocket
And we can continue adding as many properties into our @Model
definition
as we need. For example one named private
of type boolean
. And so on, and
so on, until we can access enough information via type-safe Java getters.
The remaining, not listed, properties are silently ignored.
Parsing Nested Objects
We can see that the JSON format contains a nested object named owner
. How can we
parse such a structure in a type-safe way? Just define yet another model definition
for it:
@Model(className="RepositoryInfo", properties = {
@Property(name = "id", type = int.class),
@Property(name = "name", type = String.class),
@Property(name = "owner", type = Owner.class),
@Property(name = "private", type = boolean.class),
})
public final class Main {
@Model(className = "Owner", properties = {
@Property(name = "login", type = String.class)
})
static final class OwnerCntrl {
}
// the main method...
}
With the above definition one can call getOwner().getLogin()
to obtain the
login information from a nested object. In addition to that, one can also
access an array of values by specifying @Property(array = true, ...)
when
defining the JSON structure we want to access from Java. Here is the final
code of our class:
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import net.java.html.BrwsrCtx;
import net.java.html.json.Model;
import net.java.html.json.Models;
import net.java.html.json.Property;
import org.netbeans.html.context.spi.Contexts;
@Model(className="RepositoryInfo", properties = {
@Property(name = "id", type = int.class),
@Property(name = "name", type = String.class),
@Property(name = "owner", type = Owner.class),
@Property(name = "private", type = boolean.class),
})
public final class Main {
@Model(className = "Owner", properties = {
@Property(name = "login", type = String.class)
})
static final class OwnerCntrl {
}
private Main() {
}
public static void main(String... args) throws Exception {
BrwsrCtx ctx = parsingContext();
FileInputStream is = new FileInputStream("/tmp/jersey.json");
List<RepositoryInfo> arr = new ArrayList<>();
Models.parse(ctx, RepositoryInfo.class, is, arr);
System.err.println("all parsed data: " + arr);
RepositoryInfo first = arr.get(0);
System.err.println("id: " + first.getId());
System.err.println("name: " + first.getName());
System.err.println("private: " + first.isPrivate());
System.err.println("owner.login: " + first.getOwner().getLogin());
}
}
Here is the final output:
all parsed data: [{"id":6109440,"name":"hol-sse-websocket","owner":{"login":"jersey"},"private":false}, {"id":4368712,"name":"jersey","owner":{"login":"jersey"},"private":false}, {"id":14029306,"name":"jersey-1.x","owner":{"login":"jersey"},"private":false}, {"id":4522462,"name":"jersey-1.x-old","owner":{"login":"jersey"},"private":false}, {"id":911627,"name":"jersey-old","owner":{"login":"jersey"},"private":false}]
id: 6109440
name: hol-sse-websocket
private: false
owner.login: jersey
Parsing JSON in a type-safe way in Java has never been easier!
Appendix: Setting the Context
The last thing to mention is the way to setup our parsing context. It is a bit magical, but as it is done just once, when setting up the classpath of the whole application, it can be hidden, for example, in the following method:
private static BrwsrCtx parsingContext() {
Contexts.Builder builder = Contexts.newBuilder("tyrus");
Contexts.fillInByProviders(Main.class, builder);
return builder.build();
}
What does it do? Well it uses the ‘magic string’ “tyrus” to request the Java
implementation of JSON parser
which is provided by the ‘ko-ws-tyrus’ module
(that is the reason why we added a dependency on this module in our pom.xml
).
This is exactly the kind of configuration done by Jersey Faces
to register proper entity convertors, so one can use the classes
generated by the @Model
annotation inside of Jersey’s @GET
, etc. methods
just by including the appropriate JAR on the Jersey application classpath.
Once again, enjoy DukeScript everywhere!