Skip to content
Pete Hug edited this page Oct 29, 2015 · 13 revisions

wjelement-cpp

Lightweight C++ wrapper for wjelement

(with JSON Schema draft04 support)

wjelement-cpp provides a simple, lightweight wrapper around the excellent wjelement C library. The wrapper supports JSON Schema draft-04.

While wjelement is fast and memory-efficiency, this C++ wrapper makes it far more convenient to use and the code is easier to read.

The WJPP::Node class is to wjelement-cpp what WJElement (a pointer to a WJElementPublic struct) is to wjelement. You can pass a WJElement to a WJPP::Node constructor and you now have a C++ interface.

Here is a simple example:

#include <wjelement++.h>
#include <iostream>

using namespace WJPP;
using namespace std;

int main(int argc, char** argv)
{
  Node people = Node::parseJson("[{\"name\":\"john\",\"male\":true,\"children\":\"mary\",\"anna\",\"josh\"],\"age\":40,\"partner\":\"rosie\"},\"name\":\"rosie\",\"male\":false,\"children\":\"mary\",\"anna\",\"josh\"],\"age\":37,\"partner\":\"john\"}]");
  
  for (Node::iterator i = people.begin(); i != people.end(); i++)
  {
    Node person = *i;
    
    cout << "===============================================================================" << endl;
    cout << "Name:     " << person["name"] << endl;
    cout << "Age:      " << person["age"] << endl;
    cout << "Sex:      " << (person["male"].getBool() ? "male" : "female") << endl;
    cout << "Partner:  " << person["partner"] << endl;
    cout << "Kids:     ";
    
    Node kids = person["children"];
    
    for (Node::iterator j = kids.begin(); j != kids.end(); j++)
      cout << *j << " ";
    
    cout << endl;
  }

  people.discard(); // frees memory
}

The WJPP::Node class simply wraps a WJElement. It is therefore highly efficient to pass a Node by value.

NULL Node

The wjelement library functions often return a WJElement which may be NULL. wjelement-cpp has many methods which return a Node even though the operation may potentially fail. For example, operator[](char*name) returns the Node of an object with the given name. So what happens if the message is sent to an Node which is not an object or the object doesn't have a property with the given name? The Node returned will wrap a NULL WJElement and therefore, operator!() will return true.

This allows you to write code like:

Node x  = people[2]["children"]["partner"]["age"];
if (x) { /* we'll never get here because x is NULL (or more correctly wraps a NULL WJElement) */}
if (!x) { /* we end up in here */}

This won't throw an exception even though people[2] already returns a Node that wraps a NULL WJElement.

Selectors

wjelement-cpp does not use or even honour wjelement like selectors:

people["[1].children"];  // returns a NULL Node
people[1]["children"];   // fine

Iterators

WJPP::Node implements a simple C++ standard library like non-const forward iterator. It works on all nodes, but unless the Node wraps an array or object, WJPP::Node::begin() will always return the same as WJPP::Node::end(). The iterator is the most efficient way to process each element in a collection.

Exceptions

WJPP::Node rarely throws std::runtime_error exceptions. Exceptions may be raised when you...

  • parse JSON
  • attach/detach a node
  • add a node to a node that already has that property
  • add a Node to a Node that is neither an array nor an object
  • request a value that is not compatible with the Node's type (for example Node is string and you send a getBool() message)
  • JSON schema based operations

JSON Schema draft 04 support

wjelement-cpp supports the JSON Schema draft 04. In the test folder you'll find a draf4-tests subfolder with all the test cases shipped with draft 04. In the test folder itself, you will find wjelement-test (a wjelement-cpp client application) which simply processes all the test cases in draf4-tests and prints the result to std::cout.

Cache

WJPP::Cache is a singleton class which is responsible for storing schema details. The only instance of the class is created when you first request the singleton via Cache::GetCache(). During construction of the only singleton class instance, two schemas are created:

  • the draft 04 meta schema (accessible via cache.getMetaSchema())
  • an empty schema (accessible via cache.getEmptySchema())

These schemas are hard coded into wjelement-cpp. The draft 04 meta schema allows you to load and validate draft 04 compliant schemas which can then be used to validate instance data.

Here is a version of the example shown above which uses schema validation:

#include <wjelement++.h>
#include <iostream>

using namespace WJPP;
using namespace std;

int main(int argc, char** argv)
{
  /*
    We obtain a reference to the global JSON schema cache by calling Cache::GetCache.
    The first time this is called the cache is created and the JSON Schema Draft-04
    meta schema is loaded. Any schemas you load will be stored in the cache. The cache
    is cleaned on exit.
  */
  Cache&        cache = Cache::GetCache();
  /*
    A hard coded sample Draft-04 compliant JSON schema
  */
  string        strSchema=
    "{\n"
    "\t\"$schema\": \"http://json-schema.org/draft-04/schema#\",\n"
    "\t\"type\": \"array\",\n"
    "\t\"items\": {\n"
    "\t\t\"type\": \"object\",\n"
    "\t\t\"properties\": {\n"
    "\t\t\t\"name\": { \"type\": \"string\" },\n"
    "\t\t\t\"age\": { \"type\": \"integer\" },\n"
    "\t\t\t\"male\": { \"type\": \"boolean\" },\n"
    "\t\t\t\"partner\": { \"type\": \"string\" },\n"
    "\t\t\t\"children\": { \n"
    "\t\t\t\t\"type\": \"array\",\n"
    "\t\t\t\t\"items\": { \"type\": \"string\" },\n"
    "\t\t\t\t\"uniqueItems\": true\n"
    "\t\t\t}\n"
    "\t\t}\n"
    "\t}\n"
    "}\n";

  /*
    Declare an errors node which is NULL initially. Note, a ManagedNode is
    just like a Node except on exit it deletes the attached JSON structure.
  */
  ManagedNode errors;

  /*
    Parse the schemas JSON. Since the schema will end up in the schema Cache
    we do not make it a ManagedNode.
  */
  Node schema = Node::parseJson(strSchema);

  /*
    Get the meta schema from the cache to validate the parsed schema and store
    potential errors in errors node.
  */
  cache.getMetaSchema().validateInstance(schema, errors);

  if (errors)
  {
    /* 
      print errors to cout and stop (note it is our responsibility to discard
      the errors to avoid leaks).
    */
    cout << "Errors in schema:";
    errors.dump(cout);

    /*
      schema needs to be discarded explicitly because erroneous schemas will
      not be cached
    */
    schema.discard();
  }
  else
  {
    /*
      Parse a document. We assign this to a managed node which will automatically
      discard the entire document during destruction.
    */
    ManagedNode people = Node::parseJson("[{\"name\":\"john\",\"male\":\"yes\",\"children\":[\"mary\",\"anna\",\"josh\"],\"age\":40,\"partner\":\"rosie\"},{\"name\":\"rosie\",\"male\":false,\"children\":[\"mary\",\"anna\",\"josh\"],\"age\":37,\"partner\":\"john\"}]");

    /*
      Get the schema to validate the data. If errors were found, write these to
      cout and stop (note it is our responsibility to discard the errors to 
      avoid leaks).
    */
    schema.validateInstance(people, errors);

    if (errors)
    {
      cout << "Errors in instance:";
      errors.dump(cout);
    }
    else
    {
      for (Node::iterator i = people.begin(); i != people.end(); i++)
      {
        Node person = *i;

        cout << "===============================================================================" << endl;
        cout << "Name:     " << person["name"] << endl;
        cout << "Age:      " << person["age"] << endl;
        cout << "Sex:      " << (person["male"].getBool() ? "male" : "female") << endl;
        cout << "Partner:  " << person["partner"] << endl;
        cout << "Kids:     ";

        Node kids = person["children"];

        for (Node::iterator j = kids.begin(); j != kids.end(); j++)
          cout << *j << " ";

        cout << endl;
      }
    }
  }
}

If you run the example above, you will get this output:

Errors in instance:
[
  {
    "schema":"#/items/properties/male",
    "instance":"#/*/male",
    "message":"expecting type boolean but found type string"
  }
]

The reason is that I made a small change to the instance JSON from the original example: I changed the first persons "male" value from true to "yes". This of course violates the schema which demands that property "male" is a boolean. Errors always have these properties:

  • schema A JSON pointer to the schema element that was used to validate the instance data
  • instance A JSON pointer to the instance data that was found to be faulty (please note that array elements are denoted with an asterix so in the error example above, the JSON pointer #/*/male could refer to #/0/male and/or #/1/male; using an asterix simply ensures that you don't get flooded with errors if the same error applies to each element in a very large array)
  • message The problem that was found.

Further Reading

You can find code documentation here: http://www.d3.org.nz/wjelement-cpp/