Qt中c++结构体与json互转

模型类的定义和转换

考虑以下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 {
        
    }
}

这时,JsonDumpInterfacefromJson只需要对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()函数只能处理我们的基础类型: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的子类型,需要单独处理。使用:

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是没法编译通过的,问题就出在dynamic_cast(&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));
}

实现readerCasterfindNextRouter函数:

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>这种嵌套类型是无法编译通过的,原因在于findByRouter对列表的处理只处理了一层,在findNextRouter(jsonValue[arrayIndex], router.mid(1))这里如果内部类型不是JsonDumpInterface类型,就会调用findNextRouter(typename std::enable_if::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

你可能感兴趣的:(Qt,c++,qt,json)