JSON Bindings

Using Avro JSON Bindings
Using a JSON Binding with a JSON Record

The JsonAvroBinding interface represents values as instances of JsonRecord. A single schema binding is created using AvroCatalog.getJsonBinding(). A multiple schema binding is created using AvroCatalog.getJsonMultiBinding().

The most important reason to use a JSON binding is for interoperability with other components or external systems that use JSON objects. This is because JSON bindings expose JSON objects, which can be managed with the Jackson API; a popular API used to manage JSON records.

Like generic bindings, a JSON binding treats values generically. And, like generic bindings, the schemas used in the application need not be fixed at build time if the schemas are treated dynamically.

However, unlike GenericRecord, certain Avro data types are not represented conveniently using the JSON syntax:

For this reason, applications that use the JSON binding should limit the data types used in their Avro schemas, and should treat the above data types carefully.

Like GenericRecord, a JSON object does not provide type safety. It can be error prone, because fields are accessed by string name and so data types cannot be checked at compile time.

Using Avro JSON Bindings

To use schema encapsulated in a generated Avro-specific class, you first provide the schema to the store as described in Managing Avro Schema in the Store.

In your Oracle NoSQL Database client code, you must make the schema available to the code. Do this by reading the schema directly from the file where you created it. For example, suppose you had the following schema defined in a file named my-schema.avsc:

{
    "type": "record",
    "name": "MyInfo",
    "namespace": "avro",
    "fields": [
        {"name": "ID", "type": "int"}
    ]
} 

Then to read that schema into your client code:

package avro;

import java.io.File;
import org.apache.avro.Schema;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;

import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.JsonAvroBinding;
import oracle.kv.avro.JsonRecord;

...

final Schema.Parser parser = new Schema.Parser();
parser.parse(new File("my-schema.avsc")); 
final Schema myInfoSchema = 
    parser.getTypes().get("avro.MyInfo"); 

Before you can begin serializing and deserializing values that use the Avro data format, you must create a JSON binding and then create an Avro record for that binding.

/**
 * For the sake of brevity, we skip the necessary steps of 
 * declaring and opening the store handle.
 */
final AvroCatalog catalog = store.getAvroCatalog();
final JsonAvroBinding binding = 
    catalog.getJsonBinding(myInfoSchema);

Once you have the binding, you need a way for your application to represent the fields in the schema, so that they can be read and written. You do this by creating a JSON record and from there an Object Node, which is a data structure that allows you to read and/or write the fields in the schema.

For example, assume we performed a store read, and now we want to examine the information stored with the Oracle NoSQL Database record.

/**
  * Assume a store read was performed here, and resulted in a 
  * ValueVersion instance called 'vv'. Then, to deserialize
  * the value in the returned record:
  */
final JsonRecord record = binding.toObject(vv.getValue());
final ObjectNode member = (ObjectNode) record.getJsonNode();
/* Retrieve the contents of the ID field. Because we are 
 * using a generic binding, we must type cast the retrieved
 * value.
 */
final int ID = member.get("ID").getIntValue(); 

If we want to write to a field (that is, we want to serialize some data), we use the member's put() method. As an example, suppose we wanted to create a brand new object to be written to the store. Then:

final ObjectNode member2 = JsonNodeFactory.instance.objectNode();
member2.put("ID", 100011);
final JsonRecord record2 = new JsonRecord(member2, myInfoSchema);
...
/**
  * Assume the creation of a store key here.
 */
...
store.put(key, binding.toValue(record2)); 

Using a JSON Binding with a JSON Record

The most common usage of the JSON binding is to store information that is already marked up with JSON. A typical example would be data collected from a web server in JSON format that you then want to place in the store.

Because Oracle NoSQL Database's Avro implementation relies on the Jackson API, which is a common API used to parse JSON records, everything you need is already in place to put a JSON record into the store.

Suppose you had a JSON record that looked like this:

{
    "name": {
        "first": "Percival",
        "last":  "Lowell"
    },
    "age": 156,
    "address": {
        "street": "Mars Hill Rd",
        "city": "Flagstaff",
        "state": "AZ",
        "zip": 86001
    }
} 

To support records of this type, you need a schema definition:

{
    "type": "record",
    "name": "MemberInfo",
    "namespace": "avro",
    "fields": [
        {"name": "name", "type": {
            "type": "record",
            "name": "FullName",
            "fields": [
                {"name": "first", "type": "string"},
                {"name": "last", "type": "string"}
            ]
        }},
        {"name": "age", "type": "int"},
        {"name": "address", "type": {
            "type": "record",
            "name": "Address",
            "fields": [
                {"name": "street", "type": "string"},
                {"name": "city", "type": "string"},
                {"name": "state", "type": "string"},
                {"name": "zip", "type": "int"}
            ]
        }}
    ]
} 

Assuming that you added that schema to a file called MemberInfo.avsc, you add it to the store using the CLI (using the ddl add-schema command), and then create your JSON binding in the same way as shown in the previous example.

(Note that one additional class is required for this example: ObjectMapper. In addition, we use java.io.BufferedReader and java.io.FileReader to support reading the JSON record from disk, which is something that you probably would not do in production client code.)

package avro;

import java.io.BufferedReader;
import java.io.File;
import org.apache.avro.Schema;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;

import oracle.kv.avro.AvroCatalog;
import oracle.kv.avro.JsonAvroBinding;
import oracle.kv.avro.JsonRecord;

import oracle.kv.Key;
import oracle.kv.Value;

...

final Schema.Parser parser = new Schema.Parser();
parser.parse(new File("MemberInfo.avsc")); 
final Schema memberInfoSchema = 
    parser.getTypes().get("avro.MemberInfo"); 

...

/**
 * For the sake of brevity, we skip the necessary steps of 
 * declaring and opening the store handle.
 */

...

final AvroCatalog catalog = store.getAvroCatalog();
final JsonAvroBinding binding = 
    catalog.getJsonBinding(memberInfoSchema);

The next step is to read the JSON record into your application. Typically, this would occur over the network, but for simplicity, we show the record being read from a file on disk:

try {
    final String jsonText = 
        readFile(new File("MemberInfoRecord.json"));

Next, we use the Jackson API to parse the text so as to create a JSON object:

    final ObjectMapper jsonMapper = new ObjectMapper();

    final JsonNode jsonObject = jsonMapper.readTree(jsonText); 

Finally, we write the JSON object to a JSON record, which we then use to create an Oracle NoSQL Database Value.

    final JsonRecord jsonRecord = 
        new JsonRecord(jsonObject, memberInfoSchema);

    final Value value = binding.toValue(jsonRecord);

The final step is to the write the object to the store in the usual way:

    store.put(Key.fromString("/any/old/key"), value);
} catch (IOException io) {
    io.printStackTrace();
} 

To be complete, the code for the readFile() method used in this example is:

private String readFile(File f) throws IOException {
    final BufferedReader r = new BufferedReader(new FileReader(f));
    final StringBuilder buf = new StringBuilder(1000);
    String line;
    while ((line = r.readLine()) != null) {
        buf.append(line);
        buf.append("\n");
    }
    return buf.toString();
}