benjiPosted under Java, JavaScript.

Here’s a neat trick to transform JSON into Java objects that implement an interface with the same structure.

Java 8 comes with Nashorn – a JavaScript runtime that has a number of extensions.

One of these extensions allows you to pass a JavaScript object to a constructor of an interface to anonymously implement that interface. So for example we can do

jjs> var r = new java.lang.Runnable({ run: function() print('hello') });
jjs> r.run();
hello

n.b. the above uses a JavaScript 1.8 lambda expression, which is supported by Nashorn and allows us to omit the braces and “return” keyword in the function defintion. We could have also omitted the parentheses in the Runnable constructor call in this case.

Now let’s suppose we have a JSON file as follows

{
  "firstname":"Some",
  "lastname":"One",
  "petNames":["Fluffy","Pickle"],
  "favouriteNumber":5
}

and we want to to treat it as a Java interface as follows.

public interface Person {
  String firstname();
  String lastname();
  List<String> petNames();
  int favouriteNumber();
}

It’s trivial to convert the JSON to a JavaScript object with JSON.parse.

The only conceptual difference between the JavaScript object and the Java Interface is that in the interface the properties such as firstname and lastname are methods not literal strings. It’s easy to convert a JavaScript object such that each value is wrapped in a function definition.

We just need to define a function that iterates through each value on our JavaScript object and wraps each in a function.

// A function that takes a value and returns a function 
// which when invoked returns the original value
function createFunc(value) function() value 
// Wrap each property in a function
function iface(map) {
  var ifaceImpl = {}
  for (key in map) ifaceImpl[key] = createFunc(map[key]);
  return ifaceImpl;
}

Applying it to our JS object gives us the following

{
  "firstname":function() "Some",
  "lastname":function() "One",
  "petNames":function() ["Fluffy","Pickle"],
  "favouriteNumber":function() 5
}

This is now satisfies our Java Interface, so we can just pass it to the constructor. Putting it all together

var Person = Packages.Person; // Import Java Person Interface
var someoneElse = new Person(iface({ 
  "firstname":"Some",
  "lastname":"One",
  "petNames":["Fluffy","Pickle"],
  "favouriteNumber":5
}));
 
Person.print(someoneElse); // Pass to a Java method that accepts a Person instance.

If our JSON were originally in a text file we could use Nashorn’s scripting extensions to read the text file and convert it to an interface in the same way. This can be useful for bootstrapping a Java app without a main method – you can read a JSON config file, convert it to a typed Java interface, and start the app. This can free the Java app from dealing with JSON or argument parsing

#!/usr/bin/jjs
function createFunc(value) function() value
function iface(map) {
  var ifaceImpl = {}
  for (key in map) ifaceImpl[key] = createFunc(map[key]);
  return ifaceImpl;
}
var Settings = Packages.Settings;
var MyApplication = Packages.MyApplication;
 
// Backticks in Nashorn scripting mode works like in Bash
var settings = new Settings(iface(JSON.parse(`cat app_config.json`))); 
MyApplication.start(settings);
public interface Settings {
  String hostname();
  int maxThreads();
}
public class MyApplication {
  public static void start(Settings settings) {
    //
  }
}

One small annoyance with this is that jjs (Nashorn executable) does not seem able to accept a classpath parameter when used with a shebang #!/usr/bin/jjs . So for now you have to execute the JavaScript with

$ jjs -scripting -cp . ./example.js

There’s a complete example here

Leave a Reply

  • (will not be published)