Skip to content

Scripting JSON documents using Epsilon

This article discusses how to create, query and modify JSON documents in Epsilon programs using the JSON driver. The examples will only cover some of the Epsilon languages, but the JSON driver supports all the languages in Epsilon.

As of Epsilon 2.5.0, the JSON driver can:

  • Read and write local JSON files.
  • Read JSON documents accessible via a URI (e.g. http(s)://, file:/, jar:/).

Querying a JSON document

For this first example, we will query the GitHub REST API via HTTP, asking it about the Eclipse Epsilon project, parse its JSON response, and produce a short text report in Markdown. This example is available from the Epsilon GitHub repository.

Here is an excerpt of the JSON document (omitted parts are in ...):

{
  ...
  "description": "Epsilon is a family of Java-based scripting languages for automating common model-based software engineering tasks, such as code generation, model-to-model transformation and model validation, that work out of the box with EMF (including Xtext and Sirius), UML (including Cameo/MagicDraw), Simulink, XML and other types of models.",
  ...
  "ssh_url": "git@github.com:eclipse/epsilon.git",
  "clone_url": "https://github.com/eclipse-epsilon/epsilon.git",
  ...
  "stargazers_count": 27,
  "watchers_count": 27,
  ...
  "forks_count": 8,
  ...
  "license": {
    "key": "epl-2.0",
    "name": "Eclipse Public License 2.0",
    "spdx_id": "EPL-2.0",
    "url": "https://api.github.com/licenses/epl-2.0",
    "node_id": "MDc6TGljZW5zZTMy"
  },
  ...
  "topics": [
    "domain-specific-languages",
    "model-based-software-engineering",
    "model-driven-engineering"
  ],
  ...
}

Basic types in JSON-based models

The JSON driver implements two types for its models: JSONObjects and JSONArrays. A JSONObject is a Java Map, and a JSONArray is a Java List. You can use all their methods as usual (e.g. Map#keySet, or List#size), as well as the EOL methods for collections and maps. In addition, you can refer to the x property of a JSONObject object o with the usual syntax o.x, both for reading and setting it.

A JSON model has one root element, which most of the time will be a JSONObject or a JSONArray (although it could be a simple scalar, like an integer). If the name of the model is M, its root element can be accessed via M.root.

Example queries

Here are some queries we can run on the above document:

  • Reading the description: M.root.description
  • Reading the SSH URL: M.root.ssh_url
  • Reading the license URL: M.root.license.url
  • Counting the number of topics: M.root.topics.size()

Example EGL template

Suppose we have this EGL template:

# Eclipse Epsilon

([%=M.root.stargazers_count%] stars, [%=M.root.watchers_count%] watchers, [%=M.root.forks_count%] forks)

[%=M.root.description%]

* Clone via HTTPS: [%=M.root.clone_url%]
* Clone via SSH: [%=M.root.ssh_url%]

## License

Epsilon is licensed under the [[%=M.root.license.name%]]([%=M.root.license.url%]).

## Topics

[% for (topic in M.root.topics) {%]
* [%=topic.ftuc()%]
[% } %]

With the above JSON document, it will produce this output:

# Eclipse Epsilon

(27 stars, 27 watchers, 8 forks)

Epsilon is a family of Java-based scripting languages for automating common model-based software engineering tasks, such as code generation, model-to-model transformation and model validation, that work out of the box with EMF (including Xtext and Sirius), UML (including Cameo/MagicDraw), Simulink, XML and other types of models.

* Clone via HTTPS: https://github.com/eclipse-epsilon/epsilon.git
* Clone via SSH: git@github.com:eclipse/epsilon.git

## License

Epsilon is licensed under the [Eclipse Public License 2.0](https://api.github.com/licenses/epl-2.0).

## Topics

* Domain-specific-languages
* Model-based-software-engineering
* Model-driven-engineering

Disambiguation between Java methods and JSON properties

In some cases, the name of the property may clash with one of the Java Map / List methods. For instance, consider this JSON document:

{"keySet": [1, 2, 3]}

In this case, Model.root.keySet() would invoke the Map keySet method (returning a set containing the "keySet" string), rather than refer to the value of the keySet property in the root object.

To resolve such clashes, the JSON driver supports adding a p_ prefix to refer to the original JSON property: Model.root.p_keySet would return the JSONArray containing the three integers shown in the document (1, 2, and 3).

Creating and modifying JSON documents

Modifying the contents of a JSON model can be done through regular assignments in EOL, using Model.root and object.property as usual.

Initial example: author JSON document

For instance, suppose we run this EOL script with a JSON model called M:

M.root = new JSONObject;
M.root.name = 'Author Name';
M.root.email = 'author@example.com';
M.root.id = 1234;

M.root.accounts = Sequence { 123, 456 };

This will produce the following JSON document:

{
  "name": "Author Name",
  "email": "author@example.com",
  "id": 1234,
  "accounts": [123, 456]
}

The first line set up the root object of the JSON document, and the remaining lines set various fields on the object. The last line assigned a Sequence to the accounts of the root object: this is OK because it did not contain any JSON arrays or objects. Otherwise, we would need to use a JSONArray instead, because the JSON driver needs to keep track of the JSON model that owns each JSONArray and JSONObject, and only JSONArray and JSONObject instances can track this information.

Author JSON document with detailed accounts

Suppose that we want M.root.accounts to contain not just account IDs, but rather entire objects with their own fields. In that case, we need to use a JSONArray as mentioned before:

M.root = new JSONObject;
M.root.name = 'Author Name';
M.root.email = 'author@example.com';
M.root.id = 1234;

M.root.accounts = new JSONArray;

var firstAccount = new JSONObject;
firstAccount.id = 123;
firstAccount.followers = 10;

var secondAccount = new JSONObject;
secondAccount.id = 456;
secondAccount.followers = 20;

M.root.accounts.add(firstAccount);
M.root.accounts.add(secondAccount);

The above EOL program would produce this JSON document:

{
  "name": "Author Name",
  "email": "author@example.com",
  "id": 1234,
  "accounts": [
    {"id": 123, "followers": 10},
    {"id": 456, "followers": 20}
  ]
}

As noted above, the only difference for JSON documents is that we need to create JSONArrays when we want to store a collection of JSON arrays or objects.

Reuse of objects in JSON documents

One important detail when creating JSON documents is that the same JSON object or array could be referenced from multiple locations. Changing that object or array would affect every location in the JSON document which references it.

As an example, consider this EOL script:

M.root = new JSONObject;
M.root.x = new JSONObject;
M.root.y = M.root.x;

// This new key will also be visible from M.root.y.a
M.root.x.a = 1;

The above EOL script will produce this JSON document:

{
  "x": {"a": 1},
  "y": {"a": 1}
}

Since both M.root.x and M.root.y referenced the same JSON object, the last line of the EOL script affected both locations in the produced JSON file.

If this is undesirable, JSON models provide a deepClone method which can produce a standalone clone of any JSONObject or JSONArray. As an example, here is an EOL script which sets M.root.y to a deep clone of M.root.x:

M.root = new JSONObject;
M.root.x = new JSONObject;
M.root.y = M.deepClone(M.root.x);

// This new key will NOT be visible from M.root.y
M.root.x.a = 1;

Since M.root.y is not the same object anymore, the last line only affects M.root.x, and the script produces this JSON document:

{
  "x": {"a": 1},
  "y": {}
}

Managing the model root in declarative Epsilon scripts

When writing Epsilon scripts that create JSON objects and arrays using declarative strategies (e.g. ETL), it will be necessary to set the root of the JSON model in a post block. This is because in a JSON model, creating a JSONObject or a JSONArray will not automatically add it to the contents of the model.

As an example, here is an ETL script which transforms EMF models that conform to a Tree metamodel into JSON documents:

post {
  var sourceRoots = Source.getResource().contents; 
  Target.root = sourceRoots.get(0).equivalent();
}

rule TreeToObject transform t: Source!Tree to o: Target!JSONObject {
   o.label = t.label;
   o.children = new Target!JSONArray;
   o.children.addAll(t.children.equivalent());
}

If we did not include the post block, the JSONObjects would be created but Target.root would never be set, so we would end up with just null in the JSON document.

The above script will first transform every Tree to a JSONObject, then assign their labels and children, and finally run the post block which will assign the JSONObject equivalent to the root Tree as the root of the Target JSON document.