QSerializer: solution for simple JSON / XML serialization

Hello, Habr!

I thought that somehow it turns out unfairly - in Java, C #, Go, Python, etc. There are libraries for comfortable serialization of object data into now fashionable JSON and XML, but in C ++ they either forgot, or didn’t want to, or didn’t really need it, or it’s all complicated, or maybe all together. So I decided to fix this thing.

All details, as usual, under the cut.

image

Background


Once again, I decided to take up the next pet-project, the essence of which was client-server exchange, while the favorite server of many was RaspberryPi. Among other things, I was interested in the issue of creating "save points" - so I could as simple as possible, within the framework of the prototype, save the state of the object before exiting and recover at the next start. Due to my unreasonable hostility to Python and my very warm attitude towards Qt, I chose Qt & C ++. Writing classes and spaghetti functions for parsing JSON was still a pleasure, I needed some universal and at the same time easy solution to my problem. “We have to figure it out,” I told myself.

First, a little about the terms:
Serialization is the process of translating a data structure into a sequence of bits. The inverse of the serialization operation is the deserialization (structuring) operation — the restoration of the initial state of the data structure from a bit sequence.
Go has a very useful “native” encoding / json package that allows you to complete serialization of the object using the Marshal method and reverse structuring using Unmarshal (because of this library, I first had an incorrect idea about marshaling, but Desine sperare qui hic intras ) . Following the concepts of this package, I found another library for Java - GSON , which turned out to be a very pleasant product, it was a pleasure to use it.

I pondered what I like about these libraries and came to the conclusion that it was their ease of use. Flexible functionality and all in one call, for serialization in JSON it was enough to call the toJson method and pass a serializable object into it. However, C ++ itself does not, by default, have proper metaobject capabilities to provide enough information about the fields of a class, as is done, for example, in Java (ClassName.class).

I only liked QJson for the Qt platform , but still it didn’t quite fit into my understanding of the ease of use generated by the aforementioned libraries. So the project appeared, which will be discussed here.

Small disclaimer:such mechanisms will not solve the problem of data interpretation for you. All that you can get from them is the conversion of data into a more convenient form for you.

QSerializer Project Structure


The project and examples can be viewed on GitHub ( link to the repository ). Detailed installation instructions are also given there.

Anticipating architectural suicide, I will make a reservation that this is not the final version. Work will continue in spite of the abandoned stones, but having made allowance for wishes.
General structural dependencies of the QSerializer library

The main goal of this project is to make serialization using user-frendly data format in C ++ accessible and elementary. The key to the quality development and maintenance of the product is its architecture. I do not exclude that other ways of implementation may appear in the comments to this article, so I left a little “space for creativity”. If you change the implementation, you can either add a new implementation of the PropertyKeeper interface or change the factory methods so that you do not have to change anything in the QSerializer functions.

Field declaration


One way to collect meta-object information in Qt is to describe it in the meta-object system of Qt itself. Perhaps this is the easiest way. MOC will generate all the necessary metadata at compile time. You can call the metaObject method on the described object, which will return an instance of the QMetaObject class, which we have to work with.

To declare fields to be serialized, you need to inherit the class from QObject and include the Q_OBJECT macro in it , in order to make it clear to the MOC about qualifying the class type as the base one from QObject.

Next, the Q_PROPERTY macro describes the members of the class. We will call the property property described in Q_PROPERTY . QSerializer will ignore property without the USER flag set to true.

Why USER flag
, , QML. . , Q_PROPERTY QML QSerializer .

class User : public QObject
{
Q_OBJECT
// Define data members to be serialized
Q_PROPERTY(QString name MEMBER name USER true)
Q_PROPERTY(int age MEMBER age USER true)
Q_PROPERTY(QString email MEMBER email USER true)
Q_PROPERTY(std::vector<QString> phone MEMBER phone USER true)
Q_PROPERTY(bool vacation MEMBER vacation USER true)
public:
  // Make base constructor
  User() { }
 
  QString name;
  int age{0};
  QString email;
  bool vacation{false};
  std::vector<QString> phone; 
};

To declare non-standard user types in the Qt meta-object system, I suggest using the QS_REGISTER macro , which is defined in qserializer.h. QS_REGISTER automates the process of registering type variations. However, you can use the classic method of registering types with qRegisterMetaType < T > (). For a meta-object system, the class type ( T ) and the class pointer ( T *) are completely different types; they will have different identifiers in the general list of types.

#define QS_METATYPE(Type) qRegisterMetaType<Type>(#Type) ;
#define QS_REGISTER(Type)       \
QS_METATYPE(Type)               \
QS_METATYPE(Type*)              \
QS_METATYPE(std::vector<Type*>) \
QS_METATYPE(std::vector<Type>)  \

class User;
void main()
{
// define user-type in Qt meta-object system
QS_REGISTER(User)
...
}

QSerializer Namespace


As you can see from the UML diagram, QSerializer contains a number of functions for serialization and structuring. The namespace conceptually reflects the declarative essence of QSerializer. The embedded functionality can be accessed through the name of QSerializer, without the need to create an object anywhere in the code.

Using the example of building JSON based on the object of the User class described above, you only need to call the QSerializer :: toJson method:

User u;
u.name = "Mike";
u.age = 25;
u.email = "example@exmail.com";
u.phone.push_back("+12345678989");
u.phone.push_back("+98765432121");
u.vacation = true;
QJsonObject json = QSerializer::toJson(&u);

And here is the resulting JSON:

{
    "name": "Mike",
    "age": 25,
    "email": "example@exmail.com",
    "phone": [
        "+12345678989",
        "+98765432121"
    ],
    "vacation": true
}

There are two ways to structure an object:

  • If you need to modify an object
    User u;
    QJsonObject userJson;
    QSerializer::fromJson(&u, userJson);
  • If you need to get a new object
    QJsonObject userJson;
    User * u = QSerializer::fromJson<User>(userJson);

More examples and output can be seen in the example folder .

Keepers


To organize convenient writing and reading of declared properties, QSerializer uses Keepers classes , each of which stores a pointer to an object (QObject descendant) and one of its QMetaProperty. QMetaProperty itself is not of particular value, in fact it is only an object with a description of the property class that has been declared for the MOC. To read and write, you need a specific class object, where this property is described - this is the main thing that you need to remember.

Each serializable field during serialization is passed to the custodian of the corresponding type. Keepers are needed to encapsulate the serialization and structuring functionality for a specific implementation for a specific type of described data. I highlighted 4 types:

  • QMetaSimpleKeeper - property keeper with primitive data types
  • QMetaArrayKeeper - property keeper with arrays of primitive data
  • QMetaObjectKeeper - keeper of nested objects
  • QMetaObjectArrayKeeper - keeper of arrays of nested objects

Data stream

At the heart of primitive data custodians is the conversion of information from JSON / XML to QVariant and vice versa, because QMetaProperty works with QVariant by default.

QMetaProperty prop;
QObject * linkedObj;
...
std::pair<QString, QJsonValue> QMetaSimpleKeeper::toJson()
{
    QJsonValue result = QJsonValue::fromVariant(prop.read(linkedObj));
    return std::make_pair(QString(prop.name()), result);
}

void QMetaSimpleKeeper::fromJson(const QJsonValue &val)
{
    prop.write(linkedObj, QVariant(val));
}

Object keepers are based on the transfer of information from JSON / XML to a series of other keepers and vice versa. Such custodians work with their property as a separate object, which can also have its own custodians, their task is to collect serialized data from the property object and structure the property object according to the available data.

QMetaProperty prop;
QObject * linkedObj;
...
void QMetaObjectKeeper::fromJson(const QJsonValue &json)
{
    ...
    QSerializer::fromJson(linkedObj, json.toObject());
}

std::pair<QString, QJsonValue> QMetaObjectKeeper::toJson()
{
    QJsonObject result = QSerializer::toJson(linkedObj);;
    return std::make_pair(prop.name(),QJsonValue(result));
}

Keepers implement the PropertyKeeper interface, from which the base abstract class of keepers is inherited. This allows you to parse and compose documents in XML or JSON format sequentially from top to bottom, simply going down the described stored properties and going deeper as you descend into the embedded objects, if any, in the described propertyes, without going into details of the implementation.

PropertyKeeper Interface
class PropertyKeeper
{
public:
    virtual ~PropertyKeeper() = default;
    virtual std::pair<QString, QJsonValue> toJson() = 0;
    virtual void fromJson(const QJsonValue&) = 0;
    virtual std::pair<QString, QDomNode> toXml() = 0;
    virtual void fromXml(const QDomNode &) = 0;
};

Guardian Factory


Since all custodians implement one interface, all implementations are hidden behind a convenient screen, and a set of these implementations is provided by the KeepersFactory factory. From the object transferred to the factory, you can get a list of all declared propertyes through its QMetaObject, based on which the type of custodian is determined.

KeepersFactory Factory Implementation
    const std::vector<int> simple_t =
    {
        qMetaTypeId<int>(),
        qMetaTypeId<bool>(),
        qMetaTypeId<double>(),
        qMetaTypeId<QString>(),
    };

    const std::vector<int> array_of_simple_t =
    {
        qMetaTypeId<std::vector<int>>(),
        qMetaTypeId<std::vector<bool>>(),
        qMetaTypeId<std::vector<double>>(),
        qMetaTypeId<std::vector<QString>>(),
    };
...
PropertyKeeper *KeepersFactory::getMetaKeeper(QObject *obj, QMetaProperty prop)
{
    int t_id = QMetaType::type(prop.typeName());
    if(std::find(simple_t.begin(), simple_t.end(), t_id) != simple_t.end())
        return new QMetaSimpleKeeper(obj,prop);
    else if (std::find(array_of_simple_t.begin(),array_of_simple_t.end(), t_id) != array_of_simple_t.end())
    {
        if( t_id == qMetaTypeId<std::vector<int>>())
            return new QMetaArrayKeeper<int>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<QString>>())
            return new QMetaArrayKeeper<QString>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<double>>())
            return new QMetaArrayKeeper<double>(obj, prop);

        else if(t_id == qMetaTypeId<std::vector<bool>>())
            return new QMetaArrayKeeper<bool>(obj, prop);
    }
    else
    {
        QObject * castobj = qvariant_cast<QObject *>(prop.read(obj));
        if(castobj)
            return new QMetaObjectKeeper(castobj,prop);
        else if (QString(prop.typeName()).contains("std::vector<"))
        {
            QString t = QString(prop.typeName()).remove("std::vector<").remove(">");
            int idOfElement = QMetaType::type(t.toStdString().c_str());
            if(QMetaType::typeFlags(idOfElement).testFlag(QMetaType::PointerToQObject))
                return new QMetaObjectArrayKeeper(obj, prop);
        }
    }
    throw QSException(UnsupportedPropertyType);
}

std::vector<PropertyKeeper *> KeepersFactory::getMetaKeepers(QObject *obj)
{
    std::vector<PropertyKeeper*> keepers;
    for(int i = 0; i < obj->metaObject()->propertyCount(); i++)
    {
        if(obj->metaObject()->property(i).isUser(obj))
            keepers.push_back(getMetaKeeper(obj, obj->metaObject()->property(i)));
    }
    return keepers;
}
...


A key feature of the guardian factory is the ability to provide a complete series of guardians for an object, and you can expand the list of supported primitive types by editing constant collections with type identifiers. Each series of keepers is a kind of map for propertyes for the object. When a KeepersFactory object is destroyed, the memory allocated for the series of keepers provided by it is freed.

Limitations and Behavior

SituationBehavior
Attempt to serialize an object whose type is not inherited from QObjectCompilation error
Undeclared type when attempting serialization / struturizationQSException :: UnsupportedPropertyType exception
An attempt to serialize / structure an object with a primitive type different from that described in the simple_t and array_of_simple_t collections.QSException::UnsupportedPropertyType. , — ,
JSON/XML
propertyes, JSON/XMLpropertyes . — propertyes
JSON propertyQSException


In my opinion, the project turned out to be worthwhile, because this article was written. For myself, I concluded that there are no universal solutions, you always have to sacrifice something. By developing flexible, in terms of use, functionality, you kill simplicity, and vice versa.

I do not urge you to use QSerializer, my goal is rather my own development as a programmer. Of course, I also pursue the goal of helping someone, but in the first place - just getting pleasure. Be positive)

All Articles