模型类的定义和转换
考虑以下json字符串:
{
"name": "class1",
"room": 1,
"courses": ["math", "english", "physics", "chemistry", "biology"],
"teacher": {
"name": "Tony",
"score": 99.9
},
"students": [
{
"name": "Alice",
"age": 12,
"score_avg": 90.4,
"adept": ["math", "english"]
},
{
"name": "Bob",
"age": 13,
"score_avg": 86.1,
"adept": ["physics", "chemistry"]
}
]
}
我们最终想要转换成的结构体模型:
struct Student {
QString name;
int age;
double scoreAvg;
QStringList adept;
};
struct Teacher {
QString name;
double score;
};
struct Classes {
QString name;
int room;
QStringList courses;
Teacher teacher;
QList<Student> students;
};
在设计转换工具内之前,先考虑外部如何使用工具类进行最简单的转换:
const QByteArray jsonStr = R"(
{
"name": "class1",
"room": 1,
"courses": ["math", "english", "physics", "chemistry", "biology"],
"teacher": {
"name": "Tony",
"score": 99.9
},
"students": [
{
"name": "Alice",
"age": 12,
"score_avg": 90.4,
"adept": ["math", "english"]
},
{
"name": "Bob",
"age": 13,
"score_avg": 86.1,
"adept": ["physics", "chemistry"]
}
]
}
)";
auto doc = QJsonDocument::fromJson(jsonStr);
if (!doc.isNull()) {
auto object = doc.object();
Classes classes;
classes.fromJson(object);
}
Json类转模型结构体
现在,我们的目标是实现fromJson
函数。既然是工具类,就不能对我们定义的模型类破坏过多,fromJson
函数自然该交给基类完成:
struct JsonDumpInterface {
//将JsonObject转换为模型字段
virtual void fromJson(const QJsonObject &jsonObject) {
}
virtual ~JsonDumpInterface() = default;
};
//模型类继承该接口:
struct Classes : JsonDumpInterface {
QString name;
int room;
QStringList courses;
Teacher teacher;
QList<Student> students;
};
现在,基类JsonDumpInterface
要自动进行模型转换就需要子类以下信息:
- 模型字段对应的json的key名字
- 模型字段是可赋值的
- 模型有哪些字段
注意,这里JsonDumpInterface
对模型字段的类型是不关心的,因此,字段应该具有将json值自动转换为自己类型的能力。要实现前两点,并且对模型破坏尽可能的少,这里需要一个辅助类ConfigKey
:
template<typename T>
struct ConfigKey {
//保存json对应的key名称
QString jsonKey;
//保存存储的值
T jsonValue;
//初始化时将key传入
explicit ConfigKey(QString key)
: jsonKey(std::move(key)), jsonValue(T())
{}
//保存
void save(const QJsonValue& value) {
}
}
此时模型类Classes
字段可以修改如下:
ConfigKey<QString> name{"name"};
使用宏简化编写:
#define CONFIG_KEY(type, var) ConfigKey<type> var{#var}
struct Student : JsonDumpInterface {
CONFIG_KEY(QString, name);
CONFIG_KEY(int, age);
//字段与key名称不一致时不使用宏
ConfigKey<double> scoreAvg{"score_avg"};
CONFIG_KEY(QStringList, adept);
};
struct Teacher : JsonDumpInterface {
CONFIG_KEY(QString, name);
CONFIG_KEY(double, score);
QList<JsonReadInterface *> prop() override {
return {&name, &score};
}
};
struct Classes : JsonDumpInterface {
CONFIG_KEY(QString, name);
CONFIG_KEY(int, room);
CONFIG_KEY(Teacher, teacher);
CONFIG_KEY(QStringList, courses);
CONFIG_KEY(QList<Student>, students);
};
去模板化
JsonDumpInterface
是无法知道字段的类型的,也因此无法自动调用字段的save
函数,这时可以使用虚基类实现转发:
struct JsonReadInterface {
//读取json的key
virtual const QString& key() const = 0;
//写入值
virtual void save(const QJsonValue& value) = 0;
virtual ~JsonReadInterface() = default;
};
//让ConfigKey继承JsonReadInterface
template<typename T>
struct ConfigKey : JsonReadInterface {
QString jsonKey;
T jsonValue;
explicit ConfigKey(QString key)
: jsonKey(std::move(key)), jsonValue(T())
{}
//读取key
const QString& key() const override {
return jsonKey;
}
//保存
void save(const QJsonValue& value) override {
}
}
这时,JsonDumpInterface
中fromJson
只需要对JsonReadInterface
指针操作即可。现在的目标是让JsonDumpInterface
知道模型类的所有成员,考虑提供一个接口让模型类自己提供成员列表:
struct JsonDumpInterface {
//...
//读取模型类所有字段
virtual QList<JsonReadInterface*> prop() = 0;
};
//模型类重写prop函数
struct Classes : JsonDumpInterface {
CONFIG_KEY(QString, name);
CONFIG_KEY(int, room);
CONFIG_KEY(QStringList, courses);
CONFIG_KEY(Teacher, teacher);
CONFIG_KEY(QList<Student>, students);
QList<JsonReadInterface *> prop() override {
return {&name, &room, &courses, &teacher, &students};
}
};
这时,依次遍历成员调用保存:
struct JsonDumpInterface {
//将JsonObject转换为模型字段
virtual void fromJson(const QJsonObject &jsonObject) {
for (const auto& item : prop()) {
if (jsonObject.contains(item->key())) {
item->save(jsonObject.value(item->key()));
}
}
}
//读取模型类所有字段
virtual QList<JsonReadInterface*> prop() = 0;
virtual ~JsonDumpInterface() = default;
};
自动匹配保存值
接下来我们的目标是在save函数中,将QJsonValue
转换为对应的字段类型。QJsonValue
转换为指定类型值可以调用toXXX()
系列函数,对于我们的目标类ConfigKey
是一个模板类自然无法使用,但是有一个toVariant()
函数转成我们熟知的QVariant
类型就好办了:
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
T jsonValue;
//保存
void save(const QJsonValue& value) override {
jsonValue = value.toVariant().value<T>();
}
}
显然,QVariant::value<T>()
函数只能处理我们的基础类型:QString、int、double等等,分以下情况分开讨论:
- 基础类型
对于QVariant支持的类型直接可以转换:
void save(const QJsonValue& value) override {
jsonValue = value.toVariant().value<T>();
}
- ConfigKey类型
ConfigKey类型可以交给它自己进行转换:
void save(const QJsonValue& value) override {
dynamic_cast<JsonDumpInterface*>(&jsonValue)->fromJson(value.toObject());
}
- QList容器类型
容器类型中可能包含基础类型,也可能包含ConfigKey类型,需要借助辅助模板进行展开:
template<typename I> struct IteratorType;
template<typename I>
struct IteratorType<QList<I>> {
using type = I;
};
void save(const QJsonValue& value) override {
fromJsonValue(jsonValue, value);
}
template<typename I>
static void fromJsonValue(I& tagValue, const QJsonValue& value) {
tagValue = T();
auto values = value.toArray();
for (const auto& v : values) {
typename IteratorType<T>::type temp;
fromJsonValue(temp, v);
tagValue.append(temp);
}
}
这里,fromJsonValue(temp, v)
对容器类模板类型值进行赋值,必然会出现同样的3种情况,这时候只需要将上面两种情况的赋值改成一样的函数声明即可:
template<typename I>
static void fromJsonValue(I& tagValue, const QJsonValue& value) {
tagValue = value.toVariant().value<T>();
}
template<typename I>
static void fromJsonValue(I& tagValue, const QJsonValue& value) {
dynamic_cast<JsonDumpInterface*>(&tagValue)->fromJson(value.toObject());
}
赋值重载
经过上面3种情况讨论,现在最后的问题是save
函数如何判断调用哪一种fromJsonValue
。实现该操作的方法有两种,一种是模板函数特化,一种是函数重载。考虑到gcc兼容性,这里使用赋值函数重载的方法实现,考虑以下重载方法实现:
template<typename T>
struct JsonIdentity {
using type = T;
};
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
template<typename K>
using ValueType = typename std::conditional<std::is_base_of<JsonDumpInterface, K>::value, JsonDumpInterface, K>::type;
//保存
void save(const QJsonValue &value) override {
fromJsonValue(jsonValue, value, JsonIdentity<ValueType<T>>());
}
//基础类型赋值
template<typename I, typename K>
static void fromJsonValue(I& tagValue, const QJsonValue& value, JsonIdentity<K>) {
tagValue = value.toVariant().value<K>();
}
//ConfigKey类型
template<typename I>
static void fromJsonValue(I& tagValue, const QJsonValue& value, JsonIdentity<JsonDumpInterface>) {
dynamic_cast<JsonDumpInterface*>(&tagValue)->fromJson(value.toObject());
}
//容器类型
template<typename I, typename K>
static void fromJsonValue(I& tagValue, const QJsonValue& value, JsonIdentity<QList<K>>) {
tagValue = QList<K>();
auto values = value.toArray();
for (const auto& v : values) {
typename IteratorType<QList<K>>::type temp;
fromJsonValue(temp, v, JsonIdentity<ValueType<K>>());
tagValue.append(temp);
}
}
}
这里用了两个工具模板:ValueType
,JsonIdentity
,是为了将基础类型T
和ConfigKey类型JsonDumpInterface
区分开,否则编译器将会都认为是T
类型而无法实现重载。至此,json字符串到模型结构体的转换就完成了。
模型字段的取值和赋值
模型字段外层嵌套了ConfigKey
类型,因此使用者访问值只能通过jsonValue
取值。考虑重载=
和()
运算符将破坏性降到最低:
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
//赋值
ConfigKey& operator=(const T& v) {
jsonValue = v;
return *this;
}
//引用取值
T& operator()() {
return jsonValue;
}
//const取值
const T& operator()() const {
return jsonValue;
}
}
外部使用通过运算符操作:
Classes classes;
classes.fromJson(object);
//read
auto aliceName = classes.students().first().name();
//write
classes.teacher().score = 98.1;
模型结构体转Json类
结构体转Json的方法与赋值类似,使用同样的写法进行反向操作:
struct JsonDumpInterface {
//...
//将模型结构体转JsonObject
virtual QJsonObject dumpToJson() {
QJsonObject jsonObject;
for (const auto& item : prop()) {
jsonObject.insert(item->key(), item->value());
}
return jsonObject;
}
};
这时需要ConfigKey
提供value
函数将自己转成QJsonValue
,考虑提供接口:
struct JsonReadInterface {
//...
//value转成QJsonValue
virtual QJsonValue value() = 0;
};
ConfigKey
实现接口:
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
//value转QJsonValue
QJsonValue value() override {
return toJsonValue(jsonValue, JsonIdentity<ValueType<T>>());
}
//基础类型转换
template<typename I, typename K>
static QJsonValue toJsonValue(I& value, JsonIdentity<K>) {
return value;
}
//ConfigKey类型
template<typename I>
static QJsonValue toJsonValue(I& value, JsonIdentity<JsonDumpInterface>) {
return dynamic_cast<JsonDumpInterface*>(&value)->dumpToJson();
}
//容器类型
template<typename I, typename K>
static QJsonValue toJsonValue(I& value, JsonIdentity<QList<K>>) {
QJsonArray jsonArray;
for (auto& v : value) {
jsonArray.append(toJsonValue(v, JsonIdentity<ValueType<K>>()));
}
return jsonArray;
}
//QStringList类型
template<typename I>
static QJsonValue toJsonValue(I& value, JsonIdentity<QStringList>) {
return QJsonArray::fromStringList(value);
}
}
这里需要注意的是,QStringList
作为QList<QString>
的子类型,需要单独处理。使用:
auto json = classes.dumpToJson();
QJsonDocument doc2(json);
auto jsonDumpStr = doc2.toJson();
字符串路由
通过以上的转换机制,很容易实现字符串路由查找的功能:
//前向声明
template<typename T>
struct ConfigKey;
struct JsonDumpInterface {
//...
//通过字符串路由查找模型字段
template<typename T>
ConfigKey<T>* findByRouter(const QString& router);
JsonReadInterface* findByRouter(const QStringList& router);
};
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
}
//实现findByRouter
template<typename T>
inline ConfigKey<T> *JsonDumpInterface::findByRouter(const QString &router) {
auto routerList = router.split(".");
if (routerList.isEmpty()) {
return nullptr;
}
return dynamic_cast<ConfigKey<T>*>(findByRouter(routerList));
}
inline JsonReadInterface *JsonDumpInterface::findByRouter(const QStringList &router) {
auto& key = router.first();
for (auto item : prop()) {
if (item->key() == key) {
if (router.size() == 1) {
return item;
}
auto child = item->findByRouter(router.mid(1));
if (child != nullptr) {
return child;
}
}
}
return nullptr;
}
定义接口:
struct JsonReadInterface {
//...
//字符串路由
virtual JsonReadInterface* findByRouter(const QStringList& router) = 0;
};
ConfigKey
实现接口:
template<typename T>
struct ConfigKey : JsonReadInterface {
//...
//通过字符串路由判断是否是自己
JsonReadInterface* findByRouter(const QStringList& router) override {
return findByRouter(router, JsonIdentity<ValueType<T>>());
}
template<typename K>
JsonReadInterface* findByRouter(const QStringList& router, JsonIdentity<K>) {
if (router.length() == 1 && router.first() == jsonKey) {
return this;
}
return nullptr;
}
JsonReadInterface* findByRouter(const QStringList& router, JsonIdentity<JsonDumpInterface>) {
return dynamic_cast<JsonDumpInterface*>(&jsonValue)->findByRouter(router);
}
template<typename K>
JsonReadInterface* findByRouter(const QStringList& router, JsonIdentity<QList<K>>) {
if (router.isEmpty()) {
return nullptr;
}
bool ok;
int arrayIndex = router.first().toInt(&ok);
if (!ok) {
return nullptr;
}
if (arrayIndex < 0 || arrayIndex >= jsonValue.size()) {
return nullptr;
}
if (router.length() == 1) {
return dynamic_cast<JsonReadInterface*>(&jsonValue[arrayIndex]);
}
return jsonValue[arrayIndex].findByRouter(router.mid(1));
}
}
使用方法:
ConfigKey<int>* room = classes.findByRouter<int>("room");
ConfigKey<QString>* teacherName = classes.findByRouter<QString>("teacher.name");
ConfigKey<QList<Student>>* students = classes.findByRouter<QList<Student>>("students");
ConfigKey<int>* aliceAge = classes.findByRouter<int>("students.0.age");
ConfigKey<QStringList>* aliceAdept = classes.findByRouter<QStringList>("students.0.adept");
实现这个工具类起主要作用的是QJsonValue和QVariant的转换,再加上模板的配合就成功的完成了struct和json的互转。完整代码:https://github.com/daonvshu/qjsonutil
更新1
今天使用ConfigKey类时发现,对于基本类型的容器例如QList<int>
是没法编译通过的,问题就出在dynamic_cast<JsonReadInterface*>(&jsonValue[arrayIndex])
这句话中,很明显int
是不能被转换为JsonReadInterface
类型的,同样的jsonValue[arrayIndex].findByRouter(router.mid(1))
这里对应int
也是没有这个函数的,要解决这个问题可以考虑使用SFINAE
技术跳过基础类型的非法调用。修改findByRouter
对容器类型的处理:
template<typename K>
JsonReadInterface* findByRouter(const QStringList& router, JsonIdentity<QList<K>>) {
if (router.isEmpty()) {
return nullptr;
}
bool ok;
int arrayIndex = router.first().toInt(&ok);
if (!ok) {
return nullptr;
}
if (arrayIndex < 0 || arrayIndex >= jsonValue.size()) {
return nullptr;
}
if (router.length() == 1) {
return readerCaster<K>(&jsonValue[arrayIndex]);
}
return findNextRouter<K>(jsonValue[arrayIndex], router.mid(1));
}
实现readerCaster
和findNextRouter
函数:
template<typename K>
static JsonReadInterface* readerCaster(typename std::enable_if<std::is_base_of<JsonReadInterface, K>::value, K*>::type value) {
return dynamic_cast<JsonReadInterface*>(value);
}
template<typename K>
static JsonReadInterface* readerCaster(typename std::enable_if<!std::is_base_of<JsonReadInterface, K>::value, K*>::type) {
return nullptr;
}
template<typename K>
static JsonReadInterface* findNextRouter(typename std::enable_if<std::is_base_of<JsonDumpInterface, K>::value, K>::type& value, const QStringList& router) {
return value.findByRouter(router);
}
template<typename K>
static JsonReadInterface* findNextRouter(typename std::enable_if<!std::is_base_of<JsonDumpInterface, K>::value, K>::type&, const QStringList&) {
return nullptr;
}
更新2
今天发现,对于QList<QList<T>>
这种嵌套类型是无法编译通过的,原因在于findByRouter
对列表的处理只处理了一层,在findNextRouter<K>(jsonValue[arrayIndex], router.mid(1))
这里如果内部类型不是JsonDumpInterface
类型,就会调用findNextRouter(typename std::enable_if<!std::is_base_of<JsonDumpInterface, K>::value, K>::type&, const QStringList&)
这个函数导致返回nullptr,对此问题修改如下:
template<typename K>
JsonReadInterface* findByRouter(const QStringList& router, JsonIdentity<QList<K>>) {
return findNextRouter(jsonValue, router, JsonIdentity<QList<K>>());
}
template<typename K>
static JsonReadInterface* findNextRouter(typename std::enable_if<!std::is_base_of<JsonDumpInterface, K>::value, K>::type& value, const QStringList& router) {
return findNextRouter(value, router, JsonIdentity<K>());
}
//增加两个函数检查嵌套类型是否是容器类型
template<typename K>
static JsonReadInterface* findNextRouter(K&, const QStringList&, JsonIdentity<K>) {
return nullptr;
}
template<typename K>
static JsonReadInterface* findNextRouter(QList<K>& value, const QStringList& router, JsonIdentity<QList<K>>) {
if (router.isEmpty()) {
return nullptr;
}
bool ok;
int arrayIndex = router.first().toInt(&ok);
if (!ok) {
return nullptr;
}
if (arrayIndex < 0 || arrayIndex >= value.size()) {
return nullptr;
}
if (router.length() == 1) {
return readerCaster<K>(&value[arrayIndex]);
}
return findNextRouter<K>(value[arrayIndex], router.mid(1));
}
更新3
今天突然发现,对于xml的转换也可使用同样的一套逻辑,只是一些细节处理要比json复杂得多,这里不展示代码了,有兴趣可以去仓库看源码和示例:https://github.com/daonvshu/qjsonutil 或者 https://gitee.com/daonvshu/qjsonutil