Time for action – the player data JSON serializer

Our next exercise is to create a serializer of the same PlayerInfo structure as we used for the XML exercise, but this time the destination data format is going to be JSON.

Start by creating a PlayerInfoJSON class and give it an interface similar to the one shown in the following code:

class PlayerInfoJSON {
public:
  PlayerInfoJSON(){}
  QByteArray writePlayerInfo(const PlayerInfo &pinfo) const;
};

All that is really required is to implement the writePlayerInfo method. This method will use QJsonDocument::fromVariant() to perform the serialization; thus, what we really have to do is convert our player data to a variant. Let's add a protected method to do that:

QVariant PlayerInfoJSON::toVariant(const PlayerInfo &pinfo) const {
  QVariantList players;
  foreach(const Player &p, pinfo.players) players << toVariant(p);
  return players;
}

Since the structure is really a list of players, we can iterate the list of players, serialize each player to a variant, and append the result to QVariantList. Having this function ready, we can descend a level and implement an overload for toVariant() that takes a Player object:

QVariant PlayerInfoJSON::toVariant(const Player &player) const {
  QVariantMap map;
  map["name"]       = player.name;
  map["password"]   = player.password;
  map["experience"] = player.experience;
  map["hitpoints"]  = player.hitPoints;
  map["location"]   = player.location;
  map["position"]   = QVariantMap({ {"x", player.position.x()},
                                    {"y", player.position.y()} });
  map["inventory"]  = toVariant(player.inventory);
  return map;
}

Tip

Qt's foreach macro takes two parameters—a declaration of a variable and a container to iterate. At each iteration, the macro assigns subsequent elements to the declared variable and executes the statement located directly after the macro. A C++11 equivalent of foreach is a range that is based for construct:

for(const Player &p: pinfo.players) players << toVariant(p);

This time, we are using QVariantMap as our base type, since we want to associate values with keys. For each key, we use the index operator to add entries to the map. The position key holds a QPoint value, which is supported natively by QVariant; however, such a variant can't be automatically encoded in JSON, so we convert the point to a variant map using the C++11 initializer list. The situation is different with the inventory—again, we have to write an overload for toVariant that will perform the conversion:

QVariant PlayerInfoJSON::toVariant(const QList<InventoryItem> &items) const {
  QVariantList list;
  foreach(const InventoryItem &item, items) list << toVariant(item);
  return list;
}

The code is almost identical to the one handling PlayerInfo objects, so let's focus on the last overload of toVariant—the one that accepts Item instances:

QVariant PlayerInfoJSON::toVariant(const InventoryItem &item) const {
  QVariantMap map;
  map["type"] = (int)item.type;
  map["subtype"] = item.subType;
  map["durability"] = item.durability;
  return map;
}

There is not much to comment here—we add all keys to the map, treating the item type as an integer for simplicity (this is not the best approach in a general case, as if we serialize our data and then change the order of values in the original enumeration, we will not get the proper item types after deserialization).

What remains is to use the code we have just written in the writePlayerInfo method:

QByteArray PlayerInfoJSON::writePlayerInfo(const PlayerInfo &pinfo) const {
  QJsonDocument doc = QJsonDocument::fromVariant(toVariant(pinfo));
  return doc.toJson();
}