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

Serializing Objects in C++ Part 2 – Writing JSON

5.00/5 (1 vote)
16 Jun 2015CPOL4 min read 17.2K   247  
Part 2 in a series.

This artical originally posted here: https://dabblingseriously.wordpress.com/2015/06/16/serializing-objects-in-c-part-2-writing-json/

Introduction

A few days ago I wrote an article on how to serialize objects from C++. Serializing objects to and from C++ (Part 1), Since then I've been doing some more experiments and now have some sample code that serializes objects from C++ to JSON. If you haven't read my original article (link above) you should read it now, and that will get you started. At the end of last week's post I had one algorithm that worked for any class that had a defined reflection object in the form of a ClassDescriptor specialization. This week the first thing I want to do is hi-light some changes that I've made to that class and the template policy.

Template Policy

First here's the template policy:
C++
template<typename T>
class PrimitiveTypeDescriptor
{
};

template<typename T>
class ClassDescriptor
{
public:
   typedef PrimitiveTypeDescriptor<T> descriptor_t;
};

As you can see there is now a typedef for the class descriptor called 'descriptor_t'. This class describes the type of class that is represented by the descriptor. Doing this allows me to implement one method for classes that are primitive and one for classes of a different type.

Updated Class Descriptors

Here's some updated sample classes and their class descriptors.

C++
class AClass
{
public:
   int aValue;
   int anotherValue;
   std::string thirdValue;
};

class AClass2
{
public:
   std::string testing;
   std::string testing2;
   
   std::string moreTesting[3];
};

template<>
class ClassDescriptor<AClass>
{
public:
   typedef ClassDescriptor<AClass> descriptor_t;
   
   template<TCallback>
   void for_each_property(TCallback& callback) const
   {
      callback("aValue", &AClass::aValue);
      callback("anotherValue", &AClass::anotherValue);
      callback("thirdValue", &AClass::thirdValue);
   }
};

template<>
class ClassDescriptor<AClass2>
{
public:
   typedef ClassDescriptor<AClass2> descriptor_t;
   
   template<TCallback>
   void for_each_property(TCallback& callback) const
   {
      callback("testing", &AClass2::testing);
      callback("testing2", &AClass2::testing2);
      callback("moreTesting", &AClass2::moreTesting);
   }
};

Ok, so what's changed? First of all, the class descriptor typedef refers back to the class descriptor itself, which means that you only have to write the class descriptor template specialization once for each class you want to reflect. The other thing is that the AClass2 class and descriptor now has an array property called moreTesting. This means that we now also need to represent classes that are arrays. Here are some array type descriptors:

C++
template<typename T>
class ArrayTypeDescriptor
{
};

template<typename T>
class ArrayTypeDescriptor<std::vector<T>>
{
public:
   size_t get_size(std::vector<T>& vec) const
   {
      return vec.size();
   }
};

template<typename T>
class ClassDescriptor<std::vector<T>>
{
public:
   typedef ArrayTypeDescriptor<std::vector<T>> descriptor_t;
};

template<typename T, int N>
class ArrayTypeDescriptor<T[N]>
{
public:
   size_t get_size(T(&t)[N]) const
   {
      return N;
   }
};

template<typename T, int N>
class ClassDescriptor<T[N]>
{
public:
   typedef ArrayTypeDescriptor<T[N]> descriptor_t;
};

Ok, so what have I done here? Basically I've represented two kinds of array descriptors, each with a get_size method that you can apply to an instance of the represented type. Also note that the typedef is not the ClassDescriptor typedef. You'll see why I did this in a moment but basically it's so that you can provide different implementations for different categories of objects. Is the object a class, a primitive, or an array.

Serialization Code

So finally, here is the JSON serialization code. You'll have to forgive me: the strings aren't escaped except for including quotes, that's a job for another experiment but I hope you can see where that functionality would be inserted.

C++
template<typename T>
typename ClassDescriptor::descriptor_t GetTypeDescriptor(const T& t)
{
   return typename ClassDescriptor::descriptor_t {};
}

template<typename TStream, typename T>
void WriteJSON(TStream &stream, T& t);

template<typename TStream, typename TClass>
class WriteJSONFunctor
{
   TStream& m_stream;
   TClass& m_t;
   bool m_first;
public:
   WriteJSONFunctor(TStream& stream, TClass& t):m_stream(stream), m_t(t)
   {
      m_first = true;
   }
   
   template<typename TPropertyType>
   void operator()(const char* szProperty, TPropertyType TClass::*pPropertyOffset)
   {
      if(m_first)
      {
         m_first = false;
      }
      else
      {
         m_stream << ",";
      }
      m_stream << "\"" << szProperty << "\": ";
      WriteJSON(m_stream, m_t.*pPropertyOffset);
   }
};

template<typename TStream, typename T>
void DispatchWriteJSON(const PrimitiveTypeDescriptor& descriptor,
   TStream &stream, T& t)
{
   stream << t;
}

template<typename TStream, typename T>
void DispatchWriteJSON(const PrimitiveTypeDescriptor& descriptor,
   TStream &stream, std::string& t)
{
   //todo: escape strings here
   stream << "\"" << t << "\"";
}

template<typename TStream, typename T>
void DispatchWriteJSON(const ArrayTypeDescriptor& descriptor,
   TStream &stream, T& t)
{
   stream << "[";
   auto size = descriptor.get_size(t);
   for(int n = 0; n < size; n++)
   {
      if(n != 0)
      {
         stream << ",";
      }
      WriteJSON(stream, t[n]);
   }
   stream << "]";
}

template<typename TStream, typename T>
void DispatchWriteJSON(const ClassDescriptor& descriptor,
   TStream &stream, T &t)
{
   WriteJSONFunctor<TStream, T> functor(stream, t);
   stream << "{";
   descriptor.for_each_property(functor);
   stream << "}";
}

template<typename TStream, typename T>
void WriteJSON(TStream &stream, T& t)
{
   DispatchWriteJSON(GetTypeDescriptor(t), stream, t);
}

There's four overloads of DispatchWriteJSON: Two for primitives, including one for strings, and one for everything else, the method executed is decided on the metaclass in the first parameter, which means you don't have to write a function for every type you can think of as long as the code is going to be the same as one of these functions: you can see that the string is an exception and so we have a special overload for that. There's one function for writing collections - in this case the collection will be an array, and finally there is the one function for writing the classes, which is not that different from the one in the last blog, except for that it's output is in JSON format, the operator is not const, and there are only writes to the (now specified) stream, instead of reads.

Client Code

I'm going to put this together now and show you the client code and an output Client Code:

C++
int main(int argc, const char * argv[])
{
   AClass c1;
   c1.aValue = 1;
   c1.anotherValue = 2;
   c1.thirdValue = "this is a test";

   AClass2 c2;
   c2.testing = "ABC";
   c2.testing2 = "DEF";
   c2.moreTesting[1] = "xzxx";

   WriteJSON(std::cout, c1);
   std::cout << std::endl;
   
   WriteJSON(std::cout, c2);
   std::cout << std::endl;
   
   std::vector vec;
   vec.push_back("1A");
   vec.push_back("2B");
   vec.push_back("3C");
   WriteJSON(std::cout, vec);
   std::cout << std::endl;

   int c = 123;
   WriteJSON(std::cout, c);
   std::cout << std::endl;

   return 0;
}

Output

{"aValue": 1,"anotherValue": 2,"thirdValue": "this is a test"}
{"testing": "ABC","testing2": "DEF","moreTesting": ["","xzxx",""]}
["1A","2B","3C"]"123

Summary

So what have we accomplished here?

It's pretty clear that we can write JSON to the console now, given a few metaclasses. Our only memory allocation here is for the strings stored and in the stream, and I think it could be plausible that you could implement this whole thing without any heap allocations, which could make it really quick apart from the fact that we're using C++ streams, which since the stream type is a template parameter, we could replace.

What's Missing?

Obviously this only writes C++ classes to JSON - this might be useful to some people, but I think to be really useful we'd also want to read from JSON. The JSON format is still tightly coupled with the iteration, visitation of the object tree, so I'd like to separate that. This doesn't support polymorphic types where we have a pointer or maybe a shared_ptr to a base type - that might make it more realistic. In my next blog I'll start addressing these things

License

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