Github Repo: https://github.com/PhillipVoyle/json_h
Introduction
Last week, I posted an article exploring how you can format C++ objects to JSON using a hand coded reflection class. This week, I'm going to go further, and present a parsing and formatting framework based on that work.
Many applications call for data to be stored or sent out of process. JSON has become one of the more common serialisation standards because of its brevity and ease of implementation, however because C++ lacks a rich reflection mechanism, at least for the time being, often classes must be serialised by hand.
Background
In the first part of this series, I presented a basic reflection class and described how you can use it to implement a general serialisation procedure for objects with properties. You can find the first part of this series here: Can you Serialize objects to and from C++?.
In the second part of this series, I elaborated on that class and described how you could use the class I originally presented to format C++ objects as JSON. You can read that article here: Serializing Objects in C++ Part 2 – Writing JSON.
In today's post, I'm going to go further and present a parsing and formatting framework based on that work.
There's also a final article located here: Polymorphic JSON Serialization in C++ and you can download the latest version of my code from github https://github.com/PhillipVoyle/json_h
Using the Code
To use the code, you'll have to download it and either tell your compiler to add the include folder to your include path, or just drop the code into your source directory.
Before I start, I want to present a sample use of the code, and here it is:
#include "json/JSON.h"
class AClass2
{
public:
std::string testing;
std::string testing2;
std::string moreTesting[3];
};
BEGIN_CLASS_DESCRIPTOR(AClass2)
CLASS_DESCRIPTOR_ENTRY(testing)
CLASS_DESCRIPTOR_ENTRY(testing2)
CLASS_DESCRIPTOR_ENTRY(moreTesting)
END_CLASS_DESCRIPTOR()
void ReadWriteJSONString()
{
std::string sJSONParse =
"{\"testing\":\"ABC\","
"\"testing2\":\"DEF\","
"\"moreTesting\":[\"\",\"xzxx\",\"\"]}";
AClass2 result;
FromJSON(result, sJSONParse);
std::string sJSONWrite = ToJSON(result);
std::cout << sJSONWrite << std::endl;
if(sJSONParse == sJSONWrite)
{
std::cout << "test pass" << std::endl;
}
else
{
std::cout << "test fail";
}
}
and here is the output. The test passes, so that's nice.
{"testing":"ABC","testing2":"DEF","moreTesting":["","xzxx",""]}
test pass
If you've been following my blog, then some of this will look familiar. We have our two test classes, AClass
and AClass2
, there are class descriptor stubs, and a method that will exercise the objects using FromJSON
and ToJSON
. What might look a little different are the reflection stubs, that are now wrapped in preprocessor macros for brevity, and much of the plumbing is now hidden in JSON.h.
Before I go on, I want to describe some new features:
- Parsing, using
ReadJSON(stream, object)
and FromJSON(string, object)
- Formatting using
WriteJSON(stream, object)
and string ToJSON(object)
std::map
support providing the key type is std::string
If that's all you need to know, go ahead and download the code.
Points of Interest
Now let's look at the plumbing. Here's the class descriptor code and preprocessor macros for that class:
#ifndef cppreflect_ClassDescriptor_h
#define cppreflect_ClassDescriptor_h
template<typename T>
class PrimitiveTypeDescriptor
{
};
template<typename TClass>
class ClassDescriptor
{
public:
typedef PrimitiveTypeDescriptor<TClass> descriptor_t;
};
#define BEGIN_CLASS_DESCRIPTOR(X) \
template<> \
class ClassDescriptor<X> \
{ \
public: \
typedef X class_t; \
typedef ClassDescriptor<X> descriptor_t; \
constexpr const char* get_name() const { return #X; } \
template<typename TCallback> \
void for_each_property(TCallback& callback) const {
#define CLASS_DESCRIPTOR_ENTRY(X) callback(#X, &class_t::X);
#define END_CLASS_DESCRIPTOR() }};
template<typename T>
typename ClassDescriptor<T>::descriptor_t GetTypeDescriptor(const T& t)
{
return typename ClassDescriptor<T>::descriptor_t {};
}
template <typename TWriter, typename TClass>
class WriteObjectFunctor
{
TWriter& m_writer;
const TClass& m_t;
bool m_first;
public:
WriteObjectFunctor(TWriter& writer, const TClass& t):m_writer(writer), m_t(t)
{
m_first = true;
}
template<typename TPropertyType>
void operator()(const char* szProperty, TPropertyType TClass::*pPropertyOffset)
{
m_writer.BeginProperty(szProperty);
WriteObject(m_writer, m_t.*pPropertyOffset);
m_writer.EndProperty();
}
};
template<typename TReader, typename T>
void DispatchReadObject(const PrimitiveTypeDescriptor<T>& descriptor, TReader &reader, T& t)
{
reader.ReadValue(t);
}
template<typename TWriter, typename T>
void DispatchWriteObject(const ClassDescriptor<T>& descriptor, TWriter &writer, const T &t)
{
WriteObjectFunctor<TWriter, T> functor(writer, t);
writer.BeginObject(descriptor.get_name());
descriptor.for_each_property(functor);
writer.EndObject();
}
template <typename TReader, typename TClass>
class ReadObjectFunctor
{
TReader& m_reader;
TClass& m_t;
std::string m_sProperty;
public:
bool m_bFound;
ReadObjectFunctor(TReader& reader, TClass& t,
std::string sProperty):m_reader(reader), m_t(t), m_sProperty(sProperty)
{
m_bFound = false;
}
template<typename TPropertyType>
void operator()(const char* szProperty, TPropertyType TClass::*pPropertyOffset)
{
if(m_sProperty == szProperty)
{
ReadJSON(m_reader, m_t.*pPropertyOffset);
m_bFound = true;
}
}
};
template<typename TReader, typename T>
void DispatchReadObject(const ClassDescriptor<T>& descriptor, TReader &reader, T &t)
{
reader.EnterObject();
if(!reader.IsEndObject())
{
std::string sProperty;
reader.FirstProperty(sProperty);
for(;;)
{
ReadObjectFunctor<TReader, T> functor {reader, t, sProperty};
descriptor.for_each_property(functor);
if(!functor.m_bFound)
{
throw std::runtime_error("could not find property");
}
if(reader.IsEndObject())
{
break;
}
reader.NextProperty(sProperty);
}
}
reader.LeaveObject();
}
template<typename TReader, typename T>
void ReadObject(TReader&reader, T& t)
{
DispatchReadObject(GetTypeDescriptor(t), reader, t);
}
template<typename TWriter, typename T>
void DispatchWriteObject(const PrimitiveTypeDescriptor<T>&
descriptor, TWriter &writer, const T& t)
{
writer.WriteValue(t);
}
template<typename TWriter, typename T>
void WriteObject(TWriter& writer, const T& t)
{
DispatchWriteObject(GetTypeDescriptor(t), writer, t);
}
#endif
You'll see much of this code is similar to last week, with the exception of the reading and writing functions, which are now hidden in JSONLexer.h, JSONReader.h, and JSONWriter.h. These files deal specifically with the textual parts of the JSON format, whereas now ReadObject
, WriteObject
delegate to the different methods on the reader. Here's JSONWriter.h so you can have a look. Again, not much different from last time. Although you'll note that I've implemented the string
escaping mechanism.
#ifndef cppreflect_JSONWriter_h
#define cppreflect_JSONWriter_h
template<typename TStream>
class JSONWriter
{
TStream& m_stream;
bool m_first;
public:
JSONWriter(TStream& stream):m_stream(stream)
{
m_first = false;
}
void BeginObject(const char* name)
{
m_stream << "{";
m_first = true;
}
void EndObject()
{
m_stream << "}";
}
void BeginProperty(const char* name)
{
if(m_first)
{
m_first = false;
}
else
{
m_stream << ",";
}
m_stream << "\"" << name << "\":";
}
void EndProperty()
{
}
void WriteValue(int value)
{
m_stream << value;
}
void WriteValue(float value)
{
m_stream << value;
}
void WriteValue(const std::string& str)
{
m_stream << "\"";
for(char c :str)
{
switch(c)
{
case '\t':
m_stream << "\\t";
break;
case '\f':
m_stream << "\\f";
break;
case '\r':
m_stream << "\\r";
break;
case '\n':
m_stream << "\\n";
break;
case '\\':
m_stream << "\\\\";
case '"':
m_stream << "\\\"";
break;
case '\b':
m_stream << "\\b";
break;
default:
m_stream << c;
break;
}
}
m_stream << "\"";
}
void WriteValue(bool b)
{
if(b)
{
m_stream << "true";
}
else
{
m_stream << "false";
}
}
void BeginArray()
{
m_stream << "[";
m_first = true;
}
void EndArray()
{
m_stream << "]";
}
void BeginItem()
{
if(m_first)
{
m_first = false;
}
else
{
m_stream << ",";
}
}
void EndItem()
{
}
};
template<typename TStream>
JSONWriter<TStream> GetJSONWriter(TStream& stream)
{
return JSONWriter<TStream>(stream);
}
#endif
I've also separated the Array and Vector code into their own header files ArrayTypeDescriptor.h and VectorTypeDescriptor.h, and there is now new code that implements the std::map
logic - it now displays as a class. Here's the code for that.
#ifndef cppreflect_MapTypeDescriptor_h
#define cppreflect_MapTypeDescriptor_h
#include <map>
template<typename T>
class ClassDescriptor;
template<typename T>
class MapTypeDescriptor
{
};
template<typename T>
class ClassDescriptor<std::map<std::string, T>>
{
public:
typedef MapTypeDescriptor<std::map<std::string, T>> descriptor_t;
};
template<typename TReader, typename T>
void DispatchReadObject(const MapTypeDescriptor<std::map<std::string,
T>>& descriptor, TReader &reader, std::map<std::string, T>& t)
{
reader.EnterObject();
if(!reader.IsEndObject())
{
std::string sProperty;
reader.FirstProperty(sProperty);
for(;;)
{
ReadJSON(reader, t[sProperty]);
if(reader.IsEndObject())
{
break;
}
reader.NextProperty(sProperty);
}
}
reader.LeaveObject();
}
template<typename TWriter, typename T>
void DispatchWriteObject(const MapTypeDescriptor<std::map<std::string,
T>>& descriptor, TWriter& writer, const std::map<std::string, T>& t)
{
writer.BeginObject("map");
for(auto it = t.begin(); it != t.end(); it++)
{
writer.BeginProperty(it->first.c_str());
WriteJSON(writer, it->second);
writer.EndProperty();
}
writer.EndObject();
}
#endif
And to use the code, it's not much different from the original use case. Just use ToJSON
or FromJSON
.
std::map<std::string, std::string> test;
test["12"] = "ABCD";
test["10"] = "ABCDE";
std::string mapString = ToJSON(test);
std::cout << mapString << std::endl;
So where are we now? I've done most of the things that I wanted to do with this project. You can read and write from C++ objects, strings, maps, arrays and vectors, but there is still a couple of things left over. I still wanted to work on some code for polymorphic structures, so std::shared_ptr<T>
or T*
, it doesn't do that yet. In my next blog, I'm going to be adding polymorphism. Thanks for reading.
If you liked this blog, let me know! Leave a comment or email me here phillipvoyle@hotmail.com. This article was originally posted here:
History
- 2015-06-21 - Initial authoring
- 2015-06-28 - Version 0.2 with JSONLexer.h