Skip to content

cojson tutorial

1. Getting Started

To start using cojson parser/generator you need to define the structure of your JSON object by mapping its elements to cojson elements, bind them to members of your C++ class (see Section 2), implement the interfaces for the data streams you have, and build cojson library with your application (see Section 3).

The library provides implementation for parsing/serialization of basic types. However, your application is not limited to those types only. You can add a reader/writer for your own type and use it with cojson (see Sections 5.1, 5.2). Also, if signatures of getters/setters in your application do not match expected in cojson, you may define your own accessor with the signatures you have (see Section 5.3).

It worth to mention that cojson neither provides nor allocates any data storage. Instead your application binds the data storage to cojson elements.

2. Defining Structure

JSON structure is defined as a hierarchy of template functions. Parameters of these templates define JSON member name, data type, and the storage access method - a member pointer or getter/setter methods. There are four groups of template functions each having its specific purpose and name:

V<...>()Defines a generic value
M<...>()Defines a member
O<...>()Defines an object mapped to C++ class
P<...>()Defines an object property
short val;
V<M<name,short,&val>>().read(in);

struct Pdo { int prop; };
O<Pdo, P<Pdo, short, &Pdo::prop>>().write(out);

2.1. Values

A value is defined with template V. Please refer to Appendix I for a comprehensive description of all available variants

RFC7159 states that a JSON text is a serialized value. E.g. it is not necessarily object or array. cojson support this feature by enabling various possibilities for data bindings.

A JSON value can be bound to:

  • a property of C++ class (the preferable way),
  • a pair of getter/setter method,
  • to a static variable,
  • to a function, returning pointer to a variable,
  • a pair of getter/setter functions.

Values, bound to static variables or functions can also be organized in a JSON array or object. Such objects will further be referred as statically-bound to distinguish them from class-bound objects

short v1=1;
long v2=2;
double v3=3;
long& func() { return v2; }
double get()  { return v3; }
void set(double v)  { v3 = v; }
V<short, &v1>().write(out);         //  1,
V<long, func>().write(out);         //  2,
V<double, get, set>().write(out);   //  3.0

2.2. Objects

Class-bound objects are defined with variadic template O that accepts bounding class as the first parameter followed by the list of members. This implies that the class is defined upfront. Statically-bound objects are defined with variadic template V with list of members as its parameters.

Members of class-bound objects are defined with templates P, while members of statically-bound objects are defined with templates M. Both of them accept member name as a parameter.

2.2.1. Object Names

Direct use of string literals as templates parameters is not supported by C++11. Therefore cojson defines name as a function returning pointer to string const char*. This workaround does not allow inline name definitions, e.g. a name should be declared before its use.

short var = 1;
static constexpr const char* myname() noexcept { return "myname"; }
struct Pdo {
  static short sprop;
  long lprop;
  static constexpr const char* propname() noexcept { return "prop"; }
} pdo;
short Pdo::sprop = 2;

V<  // statically-bound object { "myname":1, "prop":2 }
    M<myname,short,&var>,
    M<Pdo::propname,short,&Pdo::sprop>
>().write(out);

O<Pdo, // class-bound object { "prop":2 }
    P<Pdo,Pdo::propname,long,&Pdo::lprop>
>().read(pdo, in);	
2.2.2. Nested Objects

Both class-bound and statically-bound classes allow nested objects. Nesting in a class-bound object is achieved with a special version of template P that accepts additional parameter - structure definition for the nested object.

struct Config {
    struct Wan {
        long uptime;
    } wan;
    static constexpr const char* name_wan() noexcept { return "wan"; }
    static constexpr const char* name_uptime() noexcept { return "uptime"; }
    static const clas<Config>& structure() noexcept {
        return
            O<Config,                                                              //{
                P<Config, name_wan, Config::Wan, &Config::wan,                     //  "wan":
                    O<Wan,                                                         //    {
                        P<Wan, name_uptime, decltype(Wan::uptime), &Wan::uptime>   //      "uptime":1
                    >                                                              //    }
            >>();                                                                  //}
    }
};

Nesting in a statically-bound objects is achieved with a is generic version of template M<name,value> where in place of value a nested definition is used.

short v1=1;
static constexpr const char* p1() noexcept { return "p1"; }
static constexpr const char* p2() noexcept { return "p2"; }
const value& nested() {
    return
    V<						//  {
      M<p1,					//    "p1" :
        V<					//      {
          M<p2,short,&v1>    //        "p2":1
        >>                   //      }
    >();                     //  }
}	
2.2.3. Object Instances

A class-bound object needs an instance of that class available for read/write operation. There is no constraints on how that instance is allocated. A statically bound object does not need any instance by itself, each of its members may access different kinds of storage and some members may require data storage allocated at compile time.

2.2.4. Extra or Missing Members

If a JSON text is missing some members defined in a cojson object, data fields bound to those members will not be updated and no error condition will be set. An empty object ({}) is a valid input for any cojson object.

Extra members, found in a JSON text, are ignored by default. However, this behavior may be altered with a library-wide configuration constant.

2.2.5. Zero Objects

As per RFC7159, a JSON object consist of zero or more name/value pairs (or members). cojson does not allow defining zero-member objects. However, as it was mentioned above, an empty JSON object ({}) is a valid input.

2.3. Arrays

cojson is designed to work with a predefined structure and this design imposes certain limitation on arrays - only homogeneous array (vector) may have unspecified length. A heterogeneous array (ordered list) must have each its item defined.

2.3.1. Ordered Lists

Ordered lists are defined with variadic template V with list of items as its parameters. Every item should also be an instantiation of template V.

short v1=1, v2=2, v3=3;
short& func() { return v2; }
short get()  { return v3; }
void set(short v)  { v3 = v; }
V<                          // [
    V<short, &v1>,          //  1,
    V<short, func>,         //  2,
    V<short, get, set>      //  3
>().write(out);             // ]
2.3.2. Vectors

Vectors are defined with template V and can be bound to a fixed-size C++ array or to a indexer - a function returning pointer to the requested item. The first form defines a fixed-length array while the second - a variable-length array.

int data[4] = { 1,2,3,4 };
unsigned count = 2;
static int* arr(unsigned i) noexcept {
	return i < count ? data + i : nullptr;
}
					
V<int,arr>().write(out);     // [1,2]
V<int,4,data>().write(out); // [1,2,3,4]
2.3.3. Arrays of Objects

Vectors are defined with template V<X,S>, where X is an accessor class and S is the object's structure definition. Two accessors - accessor::array and accessor::vector implement two possible bindings to an indexer or to a static array.

struct Item {
    static constexpr const char* name_a() noexcept { return "a"; }
    static constexpr const char* name_b() noexcept { return "b"; }
    short a, b;
};

Item Items[4];

void tutorial_example4(lexer&, ostream& out) {
    V<accessor::array<Item,4,Items>,
        O<Item,
            P<Item, Item::name_a, decltype(a), &Item::a>,
            P<Item, Item::name_b, decltype(b), &Item::b>
        >	
    >().write(out);
}
2.3.4. Nested Arrays

An ordered list may seamlessly include another list or vector.

int data[4] = { 1,2,3,4 };
unsigned count = 2;
static int* arr(unsigned i) noexcept {
	return i < count ? data + i : nullptr;
}
					
V<                      // [
    V<int,arr>,         //   [1,2]
    V<int,4,data>       //   [1,2,3,4]
>().write(out);         // ]

Other variants require more complicated technique: defining a C++ type for the inner array and implementing custom reader/writer for that type.

2.3.5. Arrays in Classes

An array field of an C++ class can be added to cojson structure with special form of template P.

struct Pdo {
    short data[4];
};
static constexpr const char* arr() noexcept { return "arr"; }
Pdo pdo;
O<Pdo,
    P<Pdo, arr, short, countof(&Pdo::data), &Pdo::data>
>().read(pdo,in);
2.3.6. Extra or Missing Items

Array items from the JSON text that do not fit cojson array definition are skipped and the overrun flag is set in the iostate::error If input array is shorter than defined in the structure, remaining items are not updated.

2.4. Numbers

A number is defined with template V that accepts the destination data and binding argument.

double ldv;
V<decltype(ldv),&ldv>().read(in);

cojson supports all fundamental numeric C++ types, except long double. When a number from JSON text does not fit the destination data type, an overflow condition occurs. By default cojson ignores overflow. This can be altered with configuration constant.

2.5. Strings

A read/write string is defined with template V that accepts string length as the first parameter and data binding (directly array or a function returning pointer). A string which is only needed for writing is defined with template V with a single parameter - function returning const char* pointer.

char str[20];
static char* data() noexcept { return str; }
static constexpr const char* info() noexcept { return "info"; }

V<20,str>().read(in);
V<10,data>().read(in);
V<info>().write(out);	

2.6. Reading/Writing

To read a JSON text you need to pass it through istream interface and lexer instance. For writing you need an ostream interface. Please refer to Section-3 for more details.

When read/write operation is complete, application, may examine stream's error() flags to identify possible errors.

If your JSON text may start with BOM sequence, call lexer.skip_bom()

#include <iostream>
#include "cojson.hpp"

using namespace cojson;
using namespace details;

class jsonw : public ostream {
private:
	std::ostream & out;
public:
	inline jsonw(std::ostream& o) noexcept : out(o) {}
	bool put(char_t c) noexcept {
		if( out.put(c).good() ) return true;
		error(details::error_t::ioerror);
		return false;
	}
};

class jsonr : public istream {
	std::istream& in;
private:
	bool get(char_t& c) noexcept {
		if( in.get(c).good() ) return true;
		if( in.eof() ) {
			error(details::error_t::eof);
			c = iostate::eos_c;
		} else {
			error(details::error_t::ioerror);
			c = iostate::err_c;
		}
		return false;
	}
public:
	inline jsonr(std::istream& i) noexcept
	  :	in(i) {}
};

using namespace std;

const char * hello() { return "Hello World!"; }
char answer[10];

int main(int, char**) {
	jsonr inp(cin);
	lexer in(inp);
	jsonw out(cout);
	V<hello>().write(out);
	in.skip_bom();
	V<sizeof(answer),answer>().read(in);
	return 0;
}	

3. Implementing Stream Interfaces

cojson provides implementation of I/O stream interfaces for character buffer only. Luckily, these interfaces need just one method each to implement.

3.1. istream Interface

	
struct my_istream : istream {
	bool get(char_t& dst) noexcept {
	/* Read a single character from the stream, place it in the dst and advance current position.
	 * Return true on success or false on error.
	 * In latter case put error code (iostate::eos_c or iostate::err_c) in the dst
	 * and set error flag with istream::error (error_t::eof or error_t::ioerror)               */
	}
};

3.2. ostream Interface

	
struct my_ostream : ostream {
	bool put(char_t c) noexcept {
	/* Write a single character to the output
	 * Returns true on success or false on error
	 * In latter case set error flag with istream::error (error_t::ioerror)    */
    }
};

4. Building

To build application with cojson, add its source directory cojson/src to the include path, add the following source files in your project:

  • cojson/src/cojson.cpp
  • cojson/src/cojson_libdep.cpp
  • cojson/src/chartypetable.cpp

With avr-g++ instead of chartypetable.cpp you may use chartypetable_progmem.cpp. It will save you 256 bytes of RAM.

On some platform you may get error about missing type_traits and limits files. These files are part of STD C++ library. If your compiler does not find them, it means STD C++ lib is not available on your platform. These files are needed for compilation only and do not introduce any run-time artifact. You may use this file from the STD C++ lib on your build machine.

$ cd cojson/include
$ ln -s /usr/include/c++/4.9/limits .
$ ln -s /usr/include/c++/4.9/type_traits .

and add cojson/include directory to the include path. Without STD C++ library you may also get linker errors about missing symbols __cxa_* operator new, operator delete.

cojson does not use new, delete, if your application does the same, you may safely stub those symbols with no implementation.

__cxa_* symbols are part of libc++ abi specification. Most critical of possibly missing symbols are __cxa_guard_acquire and __cxa_guard_release. You may find a simple implementation for them in test/tools/avrcppfix.cpp or use any other suitable implementation.

On some platforms replacing __cxa_pure_virtual with empty implementation saves sufficient amount of program memory. Perhaps this fact is caused by presence of comprehensive diagnostic messages in the built-in function.

5. Advanced Topics

cojson can be easily extended with new functionality that your application may need. This is possible along two axes - (1) parsing/serializing a value and (2) delivering the parsed value to the application.

Parsing and serializing a value is done via reader and writer templates. Delivering the value - with accessor templates.

5.1. User Defined reader

To define a reader, provide an explicit specialization for template reader with the destination type and implement method read. This method should read input text char-by-char from the lexer, transform them into the value of destination type and return true on success or false on error.

In case of an error it should not leave the lexer in the middle of a lexeme, e.g. if error is detected on the first character it should send it back with lexer.back(). If a recoverable error occurs in the middle of a lexeme, read should consume remaining characters of the lexeme and set error flag error_t::mismatch. On an irrecoverable error, such as syntax violation, reader should set error_t::bad flag and return false.

//cojson reader for std::string
#include <string>
#include "cojson.hpp"
namespace cojson {
namespace details {
template<>
bool reader<std::string>::read(std::string& dst, lexer& in) noexcept  {
	std::string tmp;
	bool first = true;
	ctype ct;
	char chr;
	while( (ct=in.string(chr, first)) == ctype::string ) {
		tmp += chr;
		first = false;
	}
	if( chr ) {
		in.error(error_t::bad);
		return false;
	}
	dst = tmp;
	return true;
}

5.2. User Defined writer

To define a writer, provide an explicit specialization for template writer with the destination type and implement method write. This method should convert input value to string and put it char-by-char to the output stream and return true on success or false on error.

Once you have reader/writer, you may used variables of that type for data bindings.

//cojson writer for std::string
template<>
bool writer<std::string>::write(const std::string& str, ostream& out) noexcept {
	return writer<const char*>::write(str.c_str(), out);
}

//using std::string
std::string str;
V<std::string, &str>().write(out);

5.3. User Defined accessor

Accessor is a C++ template that wraps various ways of getting and setting variable value into a uniform, compile-time interface. Namespace cojson::accessor contains several accessors. To create your own, copy one most suitable from existing accessor templates, and adjust its parameters method implementations and constant values as needed.

// Custom accessor via class methods.
// Differs from original by the signatures of setter/getter
template<class C, typename T, const T& (C::*G)() const,
	C& (C::*S)(const T&)>
struct mymethods {
	typedef C clas;
	typedef T type;
	static constexpr bool canget = true;
	static constexpr bool canset = true;
	static constexpr bool canlref= false;
	static constexpr bool canrref= false;
	static constexpr bool is_vector = false;
	static inline constexpr bool has() noexcept { return true; }
	static inline T get(const C& o) noexcept { return (o.*G)(); }
	static T& lref(const C& o) noexcept; 		/* not possible */
	static const T& rref(const C&) noexcept;	/* not possible */
	static inline void set(C& o, const T& v) noexcept { (o.*S)(v); }
	static inline void init(T&) noexcept { }
	static inline constexpr bool null(C&) noexcept {
		return not config::null_is_error;
	}
private:
	mymethods ();
};

// Using custom accessor
struct Do {
  const int& get() const;
  Do& set(const int&);
  static const char* name() { return "val"; }
  bool read(lexer& in) {
    return O<Do,P<Do,&Do::name,
      mymethods<Do,int,&Do::get,&Do::set>>>().read(*this,in);
  }
};

5.4. Configuring cojson

cojson is configured with a set of constexpr values grouped in the config structure and externalized into an includeable file cojson.config. You may adjust that file or create your own cojson.config file in another location, located on the include path earlier than the original cojson.config.

SymbolValuesWhen specified...
overflowsaturatenumbers are saturated on overflow
erroroverflow causes an error
ignoreoverflow condition silently ignored
mismatchskipreader makes best efforts to skip such values
errorany mismatch in size or data type is treated as an irrecoverable error
nullskipskip nulls by default
errordefault handling for null is raising irrecoverable error
iostate_notvirtualstream's error method are not virtual
_virtualstream's error method are virtual. This allows to implement better error reporting by overriding iostate::error methods
temporarystatictemporary buffer is implemented static
_automatictemporary buffer is implemented automatic (allocated on the stack)
temporary_size<number>overrides temporary buffer size
cstringavr_progmemuse progmem for defining literals and member names (AVR only)
const_charliterals and member names are defined constant strings (const char*), default
static constexpr cstring_is cstring = cstring_is::avr_progmem;

Normative References

  1. RFC7159

Informative References

  1. libc++abi Specification

Appendix I. Guide Map

JSON textC++cojson definitionComments
Statically bound values
123
long myvar;
V<long,&myvar>
Value bound to a variable. The simplest binding
345
short& myfunc() {
  static short myvar;
  return myvar;
}
V<long,myfunc>
Value bound to a function returning reference. Bindings via functions allows application to detect presence of the value in the stream and perform on-demand space allocation
678
int* myfunc() {
  static int myvar;
  return &myvar;
}
V<int,myfunc>
Value bound to a function returning pointer. In this binding function may return nullptr if value is not available. Because of this cojson calls it twice per value.
true
static bool myval;
bool my_get() { 
  return myval; }
void my_set(bool val) { 
  myval = val; }
V<bool,my_get,my_set>
Value bound to a getter/setter pair. This binding allows handling values with no storage behind and perform validation of the parsed values
"a string"
char mystring[24];
V<24,mystring>
String bound to an array of char.
"output only"
const char* msg() { 
  return "output only";
}
V<msg>
A string value available for writing only. The content does not have to be constant.
"a string"
char* data = nullptr;
static char* mystr() {
  if( data == nullptr ) 
    data = (char*) malloc(64);
  return data;}
V<64,mystr>
String binding via a function returning pointer
[1,2,3,4]
int data[4];
V<int,4,data>
Array (vector) bound to a C++ array
[1,2,3]
int data[4];
size_t count = 3;
int* func(size_t i) {
  return i<count 
    ? data+i : nullptr;
}
V<int,func>
Vector binding via a function. It allows controlling array length
[2.1, "kg"]
double val;
char unit[8];
V< V<double,&val>,
   V<8,unit>>
Heterogeneous array bound to static variables
{"v":20}
int v1;
1
NAME(v)
V< M<v, int, &v1> >
Object with a member bound to static variable.
{"u":"s"}
char u1[8];
NAME(u)
V< M<u, V<8,  u1>> >
Object with a member bound to a cojson value.
{"s":"x"}
char* data;
static char* mystr() {
  return data;
}
NAME(s)
V<M<s,16,mystr>>
Object with a string member bound to a function
{"v":4}
char& myfunc() {
  static char myvar;
  return myvar;
}
NAME(v)
V<M<v,char,myfunc>>
Member bound to a function returning reference.
{"v":5}
static double myvar;
double* myfunc() {
  return &myvar;
}
NAME(v)
V<M<v,doublemyfunc>>
Member bound to a function returning pointer.
{"led":0}
int get() {
  return PORTA&LED_BIT;}
void set(int v) {
  if(v) PORTA|=LED_BIT;
  else PORTA&=~LED_BIT;}
NAME(led)
V<M<led,
 functions<int,get,set>
>>
Member bound to a pair of functions
{"a":"info"}
const char* info() { 
  return "info";
}
NAME(a)
V<M<a,info>>
Member bound to an output only string
Class bound objects and their members
{ ... }
struct Pdo {
//...
};
O<...>
Class bound object
{"a":"15.10.20"}
struct Pdo {
  char a[16];
};
NAME(a)
O<Pdo,
  P<Pdo,16,&Pod::a>>
Member bound to a class property
{"led":true}
struct Pdo {
  bool get() { 
    return PORTA&LED_BIT;}}
  void set(bool v) { 
    if(v) PORTA|=LED_BIT;
    else PORTA&=~LED_BIT;}
};
NAME(led)
O<Pdo,
 M<Pdo,led,methods<
  bool,Pdo::get,Pdo::set>
>>
Member bound via getter/setter methods
{"a":15}
struct Pdo {
  int a;
};
NAME(a)
O<Pdo,
  P<Pdo,int,&Pod::a>>
String member bound to a class property
{"a":[1,2]}
struct Pdo {
  short a[2];
};
NAME(a)
O<Pdo, 
  P<Pdo,a,short,
    2,&Pdo::a>>
Array-vector bound to an array property of class Pdo
{"a":{"b":10}}
struct Pdo {
  struct A {
	int b;
  } a;};
NAME(a) NAME(b)
O<Pdo, P<Pdo,a,Pdo::A,&Pdo::a,
  O<Pdo::A,
    P<Pdo::A,b,int,&Pdo::A::b>>>>
Nested object
{"a":[{"b":1},
       {"b":2}]}
struct Pdo {
  struct A {
    int b;
  } a[2];
};
NAME(a) NAME(b)
O<Pdo, P<Pdo,a,Pdo::A,2,Pdo::a,
  O<Pdo::A,
    P<Pdo::A,b,int,&Pdo::A::b>>>>
Nested array of objects
[{"a":1},
 {"a":2}]
struct Pdo {
  int a;
} pdo[2];
NAME(a)
V<array<Pdo,2,pdo>,
 O<Pdo,P<a,int,&Pdo::a>>>
Array of objects

1NAME here is a macro that defines a cojson name

#define NAME(s) static inline constexpr const char* s() noexcept { return #s; }

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*

*

This blog is protected by dr Dave\'s Spam Karma 2: 200 Spams eaten and counting...