Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Reading and Writing Objects in C++ Part 3 - JSON.h

4.80/5 (7 votes)
20 Jun 2015CPOL4 min read 31.9K   507  
This article describes a C++ header only library for reflecting C++ objects and parsing and formatting JSON.

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:

C++
#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:

C++
#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.

C++
#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.

C++
#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.

C++
/* outputs {"10":"ABCDE","12":"ABCD"} */

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)