Click here to Skip to main content
15,892,537 members
Articles / Programming Languages / C++

Polymorphic JSON Serialization in C++

Rate me:
Please Sign up or sign in to vote.
4.60/5 (5 votes)
30 Jun 2015CPOL3 min read 18.3K   258   16   9
Using JSON.h (version 0.3 supporting polymorphic types and std::shared_ptr)

Introduction

This article documents an implementation of JSON which allows for the serialization and deserialization of polymorphic C++ objects deriving from an interface type. Previously I've written about how you can use a brief stub object to describe the properties exposed from a serializable C++ object. In this article I'll extend the work that I've previously described, and this time describe how abstract base classes can be serialized provided the types that derive from the base class are pre-registered. You can find the code used in this article here on github: https://github.com/PhillipVoyle/json_h

If you're interested you could also check out my previous articles here on my articles page:

Or check out my blog here: https://dabblingseriously.wordpress.com/

Using the Code

The very first thing you will do is define the interface that will be your base class. In this example I'm going to use a brief calculator example which has one virtual method Execute, and then I'm going to derive three subclasses from that abstract base class: An Add operation, Multiply, and then a terminal operation Value, which returns a value.

C++
#include "json/JSON.h"

class IOperation
{
public:
   virtual float Execute() = 0;
};

class Add : public IOperation
{
public:
   std::shared_ptr<IOperation> operand1;
   std::shared_ptr<IOperation> operand2;
   Add()
   {
   }
   Add(std::shared_ptr<IOperation> o1,
      std::shared_ptr<IOperation> o2)
      :operand1(o1), operand2(o2)
   {
   }

   float Execute()
   {
      return operand1->Execute() + operand2->Execute();
   }
};

class Multiply : public IOperation
{
public:
   std::shared_ptr<IOperation> operand1;
   std::shared_ptr<IOperation> operand2;
   Multiply()
   {
   }
   Multiply(std::shared_ptr<IOperation> o1,
      std::shared_ptr<IOperation> o2)
      :operand1(o1), operand2(o2)
   {
   }

   float Execute()
   {
      return operand1->Execute() * operand2->Execute();
   }
};

class Value: public IOperation
{
public:
   float value;

   Value()
   {
      value = 0;
   }
   Value(float v): value(v)
   {
   }

   float Execute()
   {
      return value;
   }
};

Secondly you will want to expose the classes to JSON.h using a stub based on the JSON.h preprocessor macros. As you can see, the interface descriptors are pretty much like the class descriptors.

C++
BEGIN_CLASS_DESCRIPTOR(Value)
   CLASS_DESCRIPTOR_ENTRY(value)
END_CLASS_DESCRIPTOR();

BEGIN_CLASS_DESCRIPTOR(Add)
   CLASS_DESCRIPTOR_ENTRY(operand1)
   CLASS_DESCRIPTOR_ENTRY(operand2)
END_CLASS_DESCRIPTOR()

BEGIN_CLASS_DESCRIPTOR(Multiply)
   CLASS_DESCRIPTOR_ENTRY(operand1)
   CLASS_DESCRIPTOR_ENTRY(operand2)
END_CLASS_DESCRIPTOR()

BEGIN_INTERFACE_DESCRIPTOR(IOperation)
   INTERFACE_DESCRIPTOR_ENTRY(Value)
   INTERFACE_DESCRIPTOR_ENTRY(Add)
   INTERFACE_DESCRIPTOR_ENTRY(Multiply)
END_INTERFACE_DESCRIPTOR()

Finally, you can use the reader and writer mechanisms to load or store the objects

C++
void RWInterface()
{
   std::string sJSON = "{"
      "\"type\":\"Add\","
         "\"operand1\":{\"type\":\"Multiply\","
            "\"operand1\":{\"type\":\"Value\",\"value\":10},"
            "\"operand2\":{\"type\":\"Value\",\"value\":15}}"
         ",\"operand2\":{\"type\":\"Value\",\"value\":26}}";

   std::shared_ptr<IOperation> test;
   FromJSON(test, sJSON);

   std::string sOut = ToJSON(test);
   std::cout << sOut << std::endl;

   auto result =  test->Execute();
   std::cout << "test->Execute() = " << result << std::endl;
   if((sOut == sJSON) && (result == 176))
   {
      std::cout << "test pass" << std::endl;
   }
   else
   {
      std::cout << "test fail" << std::endl;
   }
}

That was easy! For reference, here is the output

{"type":"Add","operand1":{"type":"Multiply","operand1":{"type":"Value","value":10},"operand2":{"type":"Value","value":15}},"operand2":{"type":"Value","value":26}}
test->Execute() = 176
test pass

How it works

That's all you need to know about how to serialize and deserialize polymorphic types in C++ using JSON.h, but if you're interested, I can show you some plumbing. First I'll show you the text of PointerTypeDescriptor.h, basically what this file does is implements readers and writers for std::shared_ptr. This is not enough for serializing polymorphic objects, for that we need interfaces too, which I'll show you next

C++
#ifndef cppreflect_PointerTypeDescriptor_h
#define cppreflect_PointerTypeDescriptor_h

#include <memory>

template<typename T, typename TReader>
std::shared_ptr<T> CreateAndRead(
    const ClassDescriptor<T>& descriptor,
    TReader& reader)
{
   auto result = std::make_shared<T>();
   ReadObject(reader, *result);
   return result;
}

template<typename T>
class ClassDescriptor;

template<typename T>
class PointerTypeDescriptor
{
};

template<typename T>
class ClassDescriptor<std::shared_ptr<T>>
{
public:
   typedef PointerTypeDescriptor<std::shared_ptr<T>> descriptor_t;
};

template<typename TReader, typename T>
void DispatchReadObject(
    const PointerTypeDescriptor<std::shared_ptr<T>> & descriptor,
    TReader &reader,
    std::shared_ptr<T>& t)
{
   typename ClassDescriptor<T>::descriptor_t desc;
   t = CreateAndRead(desc, reader);
}

template<typename TWriter, typename T>
void DispatchWriteObject(
    const PointerTypeDescriptor<std::shared_ptr<T>> & descriptor,
    TWriter &writer,
    const std::shared_ptr<T>& t)
{
   if(t == nullptr)
   {
      writer.WriteNull();
   }
   else
   {
      WriteObject(writer, *t.get());
   }
}

#endif

You will see a new generic in here called CreateAndRead, which takes a ClassDescriptor, and which we will now provide an override for which takes a new type InterfaceDescriptor. Below is the body of InterfaceDescriptor.h. The file contains the descriptor type as mentioned, some new preprocessor macros, which you will have seen used in the example above, and some methods for reading and writing polymorphic types. Note the use of dynamic_cast. This is an operation commonly used in higher level languages, but not so often in C++ because of the overhead. Feel free to write to me and suggest another mechanism - I only use this to determine the type of object I'm writing. Also note the override of CreateAndRead.

C++
#ifndef cppreflect_InterfaceDescriptor_h
#define cppreflect_InterfaceDescriptor_h

template<typename T>
class InterfaceDescriptor
{
};

#define BEGIN_INTERFACE_DESCRIPTOR(X) \
template<> class InterfaceDescriptor<X>; \
template<> class ClassDescriptor<X> { public: typedef InterfaceDescriptor<X> descriptor_t;}; \
template<> \
class InterfaceDescriptor<X> { public: \
   template<typename TCallback>\
   void for_each_interface(TCallback callback) const\
   {

#define INTERFACE_DESCRIPTOR_ENTRY(X) callback(ClassDescriptor<X>::descriptor_t{});

#define END_INTERFACE_DESCRIPTOR() \
   } \
};

template<typename TInterface, typename TReader>
class TryReadObjectFunctor
{
   std::shared_ptr<TInterface>& m_t;
   TReader &m_reader;
   std::string m_type;
public:
   TryReadObjectFunctor(std::shared_ptr<TInterface>& t, TReader &reader, const std::string& type):m_t(t), m_reader(reader), m_type(type)
   {
   }

   template<typename TConcrete>
   void operator()(ClassDescriptor<TConcrete> descriptor) const
   {
      if(m_type != descriptor.get_name())
      {
         return;
      }

      auto t = std::make_shared<TConcrete>();
      m_t = t;

      std::string sProperty;
      while(!m_reader.IsEndObject())
      {
         m_reader.NextProperty(sProperty);

         ReadObjectFunctor<TReader, TConcrete> functor {m_reader, *t, sProperty};
         descriptor.for_each_property(functor);
         if(!functor.m_bFound)
         {
            throw std::runtime_error("could not find property");
         }
      }
   }
};

template<typename TReader, typename T>
std::shared_ptr<T> CreateAndRead(const InterfaceDescriptor<T> & descriptor, TReader &reader)
{
   std::shared_ptr<T> result;
   reader.EnterObject();
   if(!reader.IsEndObject())
   {
      std::string sProperty;
      reader.FirstProperty(sProperty);
      if(sProperty != "type")
      {
         throw std::runtime_error("expected type property");
      }
      std::string sType;
      ReadObject(reader, sType);

      TryReadObjectFunctor<T, TReader> functor {result, reader, sType};
      descriptor.for_each_interface(functor);
   }
   reader.LeaveObject();

   return result;
}

template<typename TInterface, typename TWriter>
class TryWriteObjectFunctor
{
   const TInterface& m_t;
   TWriter &m_writer;
public:
   TryWriteObjectFunctor(const TInterface& t, TWriter &writer):m_t(t), m_writer(writer)
   {
   }

   template<typename TConcrete>
   void operator()(ClassDescriptor<TConcrete> descriptor) const
   {
      try {
         const TConcrete& concrete = dynamic_cast<const TConcrete&>(m_t); //todo: prefer faster mechanism?

         m_writer.BeginObject(descriptor.get_name());
         m_writer.BeginProperty("type");

         WriteObjectFunctor<TWriter, TConcrete> functor(m_writer, concrete, false);
         WriteObject(m_writer, std::string(descriptor.get_name())); //fixme: (allow const char*)
         m_writer.EndProperty();
         descriptor.for_each_property(functor);
         m_writer.EndObject();
      }
      catch (std::bad_cast e)
      {
         //ignore it
      }
   }
};

template<typename TWriter, typename T>
void DispatchWriteObject(const InterfaceDescriptor<T> & descriptor, TWriter &writer, const T& t)
{
   TryWriteObjectFunctor<T, TWriter> functor {t, writer};
   descriptor.for_each_interface(functor);
}

#endif

Summing Up

Ok, so now you can use C++ to write JSON objects, this was fun, but maybe a little wordy. This kind of thing still takes quite a bit of effort, but that's going to be the way it is for at least the next little while (a decade?) until the standards committee agree on a reflection standard.

In my next blog I'm hoping to use some of this stuff to host a JSON web service in a console app, using boost.asio. If you're still interested, drop by.

Thanks for reading. If you liked this article or my code, or have any other comments I'd love to hear from you, so drop me a line, or comment on this article.

This article was originally posted here: https://dabblingseriously.wordpress.com/2015/06/30/polymorphic-json-serialization-in-c/

License

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


Written By
Software Developer (Senior)
New Zealand New Zealand
I've spent time programming for the security industry, video games, and telephony. I live in New Zealand, and have a Bachelor of Computing and Mathematical Sciences specializing in Computer Technology, from the University of Waikato in Hamilton, New Zealand.

Comments and Discussions

 
QuestionUsing C++ reflection for json serialisation Pin
hpcoder22-Jul-15 15:48
hpcoder22-Jul-15 15:48 
AnswerRe: Using C++ reflection for json serialisation Pin
phillipvoyle2-Jul-15 18:45
phillipvoyle2-Jul-15 18:45 
GeneralRe: Using C++ reflection for json serialisation Pin
hpcoder22-Jul-15 19:43
hpcoder22-Jul-15 19:43 
QuestionAvoiding dynamic_cast Pin
Jim Barry1-Jul-15 7:45
Jim Barry1-Jul-15 7:45 
Although dynamic_cast probably isn't all that expensive, you can reduce the cost by using typeid (fast, constant time) followed by static_cast. Something like this:
C++
template<typename TConcrete>
void operator()(ClassDescriptor<TConcrete> descriptor) const
{
  if (typeid(TConcrete) == typeid(m_t))
  {
    const TConcrete& concrete = static_cast<const TConcrete&>(m_t);

    // ...
  }
}

AnswerRe: Avoiding dynamic_cast Pin
phillipvoyle1-Jul-15 23:33
phillipvoyle1-Jul-15 23:33 
QuestionLeak, destructor isn't virtual Pin
Member 59919321-Jul-15 7:11
Member 59919321-Jul-15 7:11 
AnswerRe: Leak, destructor isn't virtual Pin
Jim Barry1-Jul-15 7:33
Jim Barry1-Jul-15 7:33 
GeneralRe: Leak, destructor isn't virtual Pin
phillipvoyle1-Jul-15 9:27
phillipvoyle1-Jul-15 9:27 
AnswerRe: Leak, destructor isn't virtual Pin
phillipvoyle1-Jul-15 9:24
phillipvoyle1-Jul-15 9:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.