knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

library(infx)

The openBIS JSON-RPC API is powered by the Jackson JSON processor on the server side. As such, object type information is communicated via @type fields. Take as an example the class Dog defined as

public class Dog {
  private String name;
  private String breed;
  // setters and getters
}

An instance of Dog can be represented as the following JSON object

{
  "@type": "Dog",
  "name": "Rufus",
  "breed": "english shepherd"
}

where the @type filed is used by the deserializer to infer what java object should result. Furthermore, on the client side, class information is used for S3 method dispatch. This document will illustrate how typed JSON objects returned by openBIS are converted into S3 classes, how these classes can be manipulated in R and subsequently used for further openBIS queries.

Creating json_class objects

Again a feature of the Jackson serializer, @id fields are generated for all objects by an ObjectIdGenerator (an IntSequenceGenerator to be more specific), which can be used to represent relationships among objects. These object ids are employed whenever the same object is returned multiple times. This happens for example when several objects representing wells on the same plate are returned. The first WellIdentifier object will contain a PlateIdentifier object and all subsequent wells identify their parent plate via a reference to this PlateIdentifier object. A simplified example, demonstrating this structure is shown below.

response <- list(list(`@type` = "WellIdentifier",
                      `@id` = 1L,
                      row = "A",
                      col = 1L,
                      plate = list(`@type` = "PlateIdentifier",
                                   `@id` = 2L,
                                   barcode = "abcd-123")),
                 list(`@type` = "WellIdentifier",
                      `@id` = 3L,
                      row = "A",
                      col = 2L,
                      plate = 2L))

Turning the list response into a json_class S3 class can be done, using the function as_json_class() (or its alias as.json_class()). This action is applied recursively, meaning that all sub-lists, containing an @type field are turned into json_class objects as well.

json_class <- as_json_class(response)
str(json_class)

In order to resolve object references encoded by @id fields, the function resolve_references(), which also acts recursively on its input, can be used. This will duplicate all occurrences of referenced objects, introducing a memory penalty, but in turn making objects self-contained. After resolving references, the list of well objects can be subsetted and the second WellIdentifier object can be used for a further query on its own.

json_class <- resolve_references(json_class)
str(json_class)

In case the user wants to create a json_class object, the constructor json_class() is available. It can be used as follows, e.g. for the re-creation of the above json_class object.

construct <- json_class(row = "A", col = 1L,
                        plate = json_class(barcode = "abcd-123",
                                           class = "PlateIdentifier"),
                        class = "WellIdentifier")
identical(construct, json_class[[1L]])

The effect of as_json_class() can be reversed by rm_json_class() or as_list() (as well as its alias as.list()). The two functions differ in default behavior however. Where rm_json_class() removes S3 classes and writes class information into the respective @type fields, as_list(), by default simple returns its input. This somewhat odd choice is owed to the circumstance that iterating though a json_class object with lapply() or sapply() calls as.list() on its first argument.

identical(response,
          rm_json_class(json_class))
identical(as.list(json_class, keep_asis = FALSE),
          rm_json_class(json_class))

In addition to functions for creating and destroying json_class objects, several utility functions for json_class are provided as well. is_json_class() (and its alias is.json_class()) tests whether the object in question is a list inheriting the class attribute json_class and has at least one more class attribute in front of json_class, which is expected in last place (within the class vector). Similarly, check_json_class() can be used to recursively test a list structure to make sure that every node inheriting the json_class class attribute is a properly formed json_class object. Finally, the two functions get_subclass() and has_subclass() can be used to extract the sub-class and test whether a given json_class object has a specific sub-class.

test <- structure(list(a = structure(list(b = "c"),
                                     class = c("foo", "json_class")),
                       d = structure(c(e = "f"),
                                     class = c("bar", "json_class"))),
                  class = c("foobar", "json_class"))

is_json_class(test)
check_json_class(test)
is_json_class(test$a)
is_json_class(test$d)

has_subclass(test, "foobar")
get_subclass(test)

In the above example, is_json_class(test$d) returns FALSE because it is an object that is not composed as a list with attributes, but as a vector with attributes. This is also the reason why the recursive execution of is_json_class() to all contained objects that inherit from json_class in check_json_class(test) returns FALSE.

The two base R generic functions print() and c() have json_class-specific methods implemented. Combining several json_class object, using c() will yield a json_vec object, which is described in the following section. Printing is recursive and recursion depth can be controlled using the argument depth. Further printing options are width, length and a logical flag fancy for enabling fancy printing (console output is colored and UTF box characters are used for creating a tree structure instead of ASCII characters).

json_class[[1]]
print(json_class[[1]], depth = 2L)
print(json_class[[1]], depth = 2L, length = 4L)
print(json_class[[1]], depth = 2L, fancy = FALSE)

Using vectors of json_class objects

Now that class information of objects fetched from openBIS is available in R, this can be used for method dispatch of S3 generic functions. Assume we have a generic function list_datasets(). We could then implement an object-specific method for objects of type sample as list_datasets.sample() and one for objects of type experiment as list_datasets.experiment(). Depending on the class of the object passed to list_datasets(), datasets for an experiment or for a sample will be listed.

There is an issue with this approach though: listing for example datasets associated with multiple experiments. A straightforward approach could be simply iterating over the list of experiments and for each one, issuing a separate request to openBIS. A more efficient way of doing this would be to query openBIS with a single list of several experiment objects. This, however defeats S3 method dispatch, as the object on which dispatch occurs is no longer experiment but list (containing several experiments). To work around this, json_vec objects are used.

The json_vec class wraps around a list of json_class and it serves to bring the common sub-class of all child objects to the surface for using method dispatch on. Instantiation of json_vec objects is possible in several ways: using the json_vec() constructor, by coercing a list structure with as_json_vec() or by combining several json_class objects using base::c(). The reverse action can be performed, using base::as.list(). The json_vec constructor, as well as functions for coercing to json_vec accept a simplify argument. When set to TRUE (default is FALSE), json_vec objects of length 1 are simplified to json_class objects.

a <- json_class("a", class = "foo")
b <- json_class("b", class = "foo")

foo_vec <- json_vec(a, b)

str(foo_vec)

identical(foo_vec, as_json_vec(list(a, b)))
identical(foo_vec, c(a, b))

identical(as.list(foo_vec), list(a, b))

identical(a, as_json_vec(list(a), simplify = TRUE))

Utility functions available for json_vec objects include has_common_subclass() and get_subclass() to check whether all entries in a list are json_class objects with the same sub-class and to extract the common sub-class, as well as is_json_vec() (and its alias is.json_vec()), which can be used to check whether

has_common_subclass(list(a, b))

get_subclass(list(a, b))
get_subclass(foo_vec)

is_json_vec(foo_vec)
is_json_vec(list(a, b))

Finally, the base R generics for which json_vec-specific methods are provided include print(), c(), as well as accessors and assignment functions.

foo_vec
foo_vec[1]

c(foo_vec[2], foo_vec[1])

class(foo_vec[1])
class(foo_vec[[1]])

As shown in the above code block, subsetting a json_vec object preserves all class attributes. This is in analogy to base vector objects, where subsetting, for example a character vector again yields a character vector. The single element accessor [[, however yields an element of the json_vec object, which should not come as surprise given the list nature of json_vec objects.



ropensci/infx documentation built on May 14, 2022, 5:51 p.m.