diff --git a/src/corelib/serialization/qcborarray.h b/src/corelib/serialization/qcborarray.h index 8d0aceac33..4ad8100bfe 100644 --- a/src/corelib/serialization/qcborarray.h +++ b/src/corelib/serialization/qcborarray.h @@ -49,6 +49,8 @@ QT_BEGIN_NAMESPACE class QJsonArray; class QDataStream; +namespace QJsonPrivate { class Variant; } + class QCborContainerPrivate; class Q_CORE_EXPORT QCborArray { @@ -273,6 +275,7 @@ private: friend QCborValue; friend QCborValueRef; + friend class QJsonPrivate::Variant; explicit QCborArray(QCborContainerPrivate &dd) noexcept; QExplicitlySharedDataPointer d; }; diff --git a/src/corelib/serialization/qcbormap.h b/src/corelib/serialization/qcbormap.h index 7e1a4cb485..cd1824f8e7 100644 --- a/src/corelib/serialization/qcbormap.h +++ b/src/corelib/serialization/qcbormap.h @@ -54,6 +54,8 @@ typedef QHash QVariantHash; class QJsonObject; class QDataStream; +namespace QJsonPrivate { class Variant; } + class QCborContainerPrivate; class Q_CORE_EXPORT QCborMap { @@ -327,6 +329,7 @@ public: private: friend class QCborValue; friend class QCborValueRef; + friend class QJsonPrivate::Variant; void detach(qsizetype reserve = 0); explicit QCborMap(QCborContainerPrivate &dd) noexcept; diff --git a/src/corelib/serialization/qjson_p.h b/src/corelib/serialization/qjson_p.h index 4589284ef3..b6f01bff1c 100644 --- a/src/corelib/serialization/qjson_p.h +++ b/src/corelib/serialization/qjson_p.h @@ -215,6 +215,13 @@ public: } }; +class Variant +{ +public: + static QJsonObject toJsonObject(const QVariantMap &map); + static QJsonArray toJsonArray(const QVariantList &list); +}; + } // namespace QJsonPrivate QT_END_NAMESPACE diff --git a/src/corelib/serialization/qjsonarray.cpp b/src/corelib/serialization/qjsonarray.cpp index 5395319b7c..7e29bdea3f 100644 --- a/src/corelib/serialization/qjsonarray.cpp +++ b/src/corelib/serialization/qjsonarray.cpp @@ -261,11 +261,14 @@ QJsonArray QJsonArray::fromStringList(const QStringList &list) The QVariant values in \a list will be converted to JSON values. + \note Conversion from \l QVariant is not completely lossless. Please see + the documentation in QJsonValue::fromVariant() for more information. + \sa toVariantList(), QJsonValue::fromVariant() */ QJsonArray QJsonArray::fromVariantList(const QVariantList &list) { - return QCborArray::fromVariantList(list).toJsonArray(); + return QJsonPrivate::Variant::toJsonArray(list); } /*! diff --git a/src/corelib/serialization/qjsoncbor.cpp b/src/corelib/serialization/qjsoncbor.cpp index e0eab74a02..e836935f6f 100644 --- a/src/corelib/serialization/qjsoncbor.cpp +++ b/src/corelib/serialization/qjsoncbor.cpp @@ -55,6 +55,8 @@ QT_BEGIN_NAMESPACE using namespace QtCbor; +enum class ConversionMode { FromRaw, FromVariantToJson }; + static QJsonValue fpToJson(double v) { return qt_is_finite(v) ? QJsonValue(v) : QJsonValue(); @@ -89,7 +91,8 @@ static QString encodeByteArray(const QCborContainerPrivate *d, qsizetype idx, QC return QString::fromLatin1(data, data.size()); } -static QString makeString(const QCborContainerPrivate *d, qsizetype idx); +static QString makeString(const QCborContainerPrivate *d, qsizetype idx, + ConversionMode mode = ConversionMode::FromRaw); static QString maybeEncodeTag(const QCborContainerPrivate *d) { @@ -134,7 +137,8 @@ static QString encodeTag(const QCborContainerPrivate *d) return s; } -static Q_NEVER_INLINE QString makeString(const QCborContainerPrivate *d, qsizetype idx) +static Q_NEVER_INLINE QString makeString(const QCborContainerPrivate *d, qsizetype idx, + ConversionMode mode) { const auto &e = d->elements.at(idx); @@ -146,7 +150,9 @@ static Q_NEVER_INLINE QString makeString(const QCborContainerPrivate *d, qsizety return QString::number(e.fpvalue()); case QCborValue::ByteArray: - return encodeByteArray(d, idx, QCborTag(QCborKnownTags::ExpectedBase64url)); + return mode == ConversionMode::FromVariantToJson + ? d->stringAt(idx) + : encodeByteArray(d, idx, QCborTag(QCborKnownTags::ExpectedBase64url)); case QCborValue::String: return d->stringAt(idx); @@ -190,7 +196,8 @@ static Q_NEVER_INLINE QString makeString(const QCborContainerPrivate *d, qsizety return simpleTypeString(e.type); } -QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx); +QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx, + ConversionMode mode = ConversionMode::FromRaw); static QJsonValue convertExtendedTypeToJson(QCborContainerPrivate *d) { @@ -222,35 +229,37 @@ static QJsonValue convertExtendedTypeToJson(QCborContainerPrivate *d) } // We need to do this because sub-objects may need conversion. -static QJsonArray convertToJsonArray(QCborContainerPrivate *d) +static QJsonArray convertToJsonArray(QCborContainerPrivate *d, + ConversionMode mode = ConversionMode::FromRaw) { QJsonArray a; if (d) { for (qsizetype idx = 0; idx < d->elements.size(); ++idx) - a.append(qt_convertToJson(d, idx)); + a.append(qt_convertToJson(d, idx, mode)); } return a; } // We need to do this because the keys need to be sorted and converted to strings // and sub-objects may need recursive conversion. -static QJsonObject convertToJsonObject(QCborContainerPrivate *d) +static QJsonObject convertToJsonObject(QCborContainerPrivate *d, + ConversionMode mode = ConversionMode::FromRaw) { QJsonObject o; if (d) { for (qsizetype idx = 0; idx < d->elements.size(); idx += 2) - o.insert(makeString(d, idx), qt_convertToJson(d, idx + 1)); + o.insert(makeString(d, idx), qt_convertToJson(d, idx + 1, mode)); } return o; } -QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx) +QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx, ConversionMode mode) { // encoding the container itself if (idx == -QCborValue::Array) - return convertToJsonArray(d); + return convertToJsonArray(d, mode); if (idx == -QCborValue::Map) - return convertToJsonObject(d); + return convertToJsonObject(d, mode); if (idx < 0) { // tag-like type if (!d || d->elements.size() != 2) @@ -264,6 +273,15 @@ QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx) case QCborValue::Integer: return QJsonValue(e.value); case QCborValue::ByteArray: + if (mode == ConversionMode::FromVariantToJson) { + const auto value = makeString(d, idx, mode); + return value.isEmpty() ? QJsonValue() : QJsonPrivate::Value::fromTrustedCbor(value); + } + break; + case QCborValue::RegularExpression: + if (mode == ConversionMode::FromVariantToJson) + return QJsonValue(); + break; case QCborValue::String: case QCborValue::SimpleType: // make string @@ -274,10 +292,10 @@ QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx) case QCborValue::Tag: case QCborValue::DateTime: case QCborValue::Url: - case QCborValue::RegularExpression: case QCborValue::Uuid: // recurse - return qt_convertToJson(e.flags & Element::IsContainer ? e.container : nullptr, -e.type); + return qt_convertToJson(e.flags & Element::IsContainer ? e.container : nullptr, -e.type, + mode); case QCborValue::Null: case QCborValue::Undefined: @@ -294,7 +312,7 @@ QJsonValue qt_convertToJson(QCborContainerPrivate *d, qsizetype idx) return fpToJson(e.fpvalue()); } - return QJsonPrivate::Value::fromTrustedCbor(makeString(d, idx)); + return QJsonPrivate::Value::fromTrustedCbor(makeString(d, idx, mode)); } /*! @@ -429,6 +447,12 @@ QJsonArray QCborArray::toJsonArray() const return convertToJsonArray(d.data()); } +QJsonArray QJsonPrivate::Variant::toJsonArray(const QVariantList &list) +{ + const auto cborArray = QCborArray::fromVariantList(list); + return convertToJsonArray(cborArray.d.data(), ConversionMode::FromVariantToJson); +} + /*! Recursively converts every \l QCborValue value in this map to JSON using QCborValue::toJsonValue() and creates a string key for all keys that aren't @@ -471,6 +495,12 @@ QJsonObject QCborMap::toJsonObject() const return convertToJsonObject(d.data()); } +QJsonObject QJsonPrivate::Variant::toJsonObject(const QVariantMap &map) +{ + const auto cborMap = QCborMap::fromVariantMap(map); + return convertToJsonObject(cborMap.d.data(), ConversionMode::FromVariantToJson); +} + /*! Converts this value to a native Qt type and returns the corresponding QVariant. diff --git a/src/corelib/serialization/qjsonobject.cpp b/src/corelib/serialization/qjsonobject.cpp index beeb3b5a54..419021a798 100644 --- a/src/corelib/serialization/qjsonobject.cpp +++ b/src/corelib/serialization/qjsonobject.cpp @@ -205,11 +205,14 @@ QJsonObject &QJsonObject::operator =(const QJsonObject &other) The keys in \a map will be used as the keys in the JSON object, and the QVariant values will be converted to JSON values. + \note Conversion from \l QVariant is not completely lossless. Please see + the documentation in QJsonValue::fromVariant() for more information. + \sa fromVariantHash(), toVariantMap(), QJsonValue::fromVariant() */ QJsonObject QJsonObject::fromVariantMap(const QVariantMap &map) { - return QCborMap::fromVariantMap(map).toJsonObject(); + return QJsonPrivate::Variant::toJsonObject(map); } /*! @@ -231,6 +234,9 @@ QVariantMap QJsonObject::toVariantMap() const The keys in \a hash will be used as the keys in the JSON object, and the QVariant values will be converted to JSON values. + \note Conversion from \l QVariant is not completely lossless. Please see + the documentation in QJsonValue::fromVariant() for more information. + \sa fromVariantMap(), toVariantHash(), QJsonValue::fromVariant() */ QJsonObject QJsonObject::fromVariantHash(const QVariantHash &hash) diff --git a/src/corelib/serialization/qjsonvalue.cpp b/src/corelib/serialization/qjsonvalue.cpp index 34fa84cdc1..0f922f521a 100644 --- a/src/corelib/serialization/qjsonvalue.cpp +++ b/src/corelib/serialization/qjsonvalue.cpp @@ -351,7 +351,6 @@ void QJsonValue::swap(QJsonValue &other) noexcept error cases as e.g. accessing a non existing key in a QJsonObject. */ - /*! Converts \a variant to a QJsonValue and returns it. @@ -461,7 +460,11 @@ void QJsonValue::swap(QJsonValue &other) noexcept For other types not listed above, a conversion to string will be attempted, usually but not always by calling QVariant::toString(). If the conversion fails the value is replaced by a null JSON value. Note that - QVariant::toString() is also lossy for the majority of types. + QVariant::toString() is also lossy for the majority of types. For example, + if the passed QVariant is representing raw byte array data, it is recommended + to pre-encode it to \l {https://www.ietf.org/rfc/rfc4648.txt}{Base64} (or + another lossless encoding), otherwise a lossy conversion using QString::fromUtf8() + will be used. Please note that the conversions via QVariant::toString() are subject to change at any time. Both QVariant and QJsonValue may be extended in the @@ -488,8 +491,10 @@ QJsonValue QJsonValue::fromVariant(const QVariant &variant) return QJsonValue(variant.toLongLong()); Q_FALLTHROUGH(); case QMetaType::Float: - case QMetaType::Double: - return QJsonValue(variant.toDouble()); + case QMetaType::Double: { + double v = variant.toDouble(); + return qt_is_finite(v) ? QJsonValue(v) : QJsonValue(); + } case QMetaType::QString: return QJsonValue(variant.toString()); case QMetaType::QStringList: diff --git a/tests/auto/corelib/serialization/json/tst_qtjson.cpp b/tests/auto/corelib/serialization/json/tst_qtjson.cpp index b11d8c996d..ac1ef76f8c 100644 --- a/tests/auto/corelib/serialization/json/tst_qtjson.cpp +++ b/tests/auto/corelib/serialization/json/tst_qtjson.cpp @@ -163,6 +163,10 @@ private Q_SLOTS: void streamVariantSerialization(); void escapeSurrogateCodePoints_data(); void escapeSurrogateCodePoints(); + + void fromToVariantConversions_data(); + void fromToVariantConversions(); + private: QString testDataDir; }; @@ -3301,5 +3305,154 @@ void tst_QtJson::escapeSurrogateCodePoints() QVERIFY(buffer.contains(escStr)); } +void tst_QtJson::fromToVariantConversions_data() +{ + QTest::addColumn("variant"); + QTest::addColumn("json"); + QTest::addColumn("jsonToVariant"); + + QByteArray utf8("\xC4\x90\xC4\x81\xC5\xA3\xC3\xA2"); // Đāţâ + QDateTime dt = QDateTime::currentDateTimeUtc(); + QUuid uuid = QUuid::createUuid(); + + constexpr qlonglong maxInt = std::numeric_limits::max(); + constexpr qlonglong minInt = std::numeric_limits::min(); + constexpr double maxDouble = std::numeric_limits::max(); + constexpr double minDouble = std::numeric_limits::min(); + + QTest::newRow("default") << QVariant() << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("nullptr") << QVariant::fromValue(nullptr) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("bool") << QVariant(true) << QJsonValue(true) << QVariant(true); + QTest::newRow("int pos") << QVariant(123) << QJsonValue(123) << QVariant(qlonglong(123)); + QTest::newRow("int neg") << QVariant(-123) << QJsonValue(-123) << QVariant(qlonglong(-123)); + QTest::newRow("int big pos") << QVariant((1ll << 55) +1) << QJsonValue((1ll << 55) + 1) + << QVariant(qlonglong((1ll << 55) + 1)); + QTest::newRow("int big neg") << QVariant(-(1ll << 55) + 1) << QJsonValue(-(1ll << 55) + 1) + << QVariant(qlonglong(-(1ll << 55) + 1)); + QTest::newRow("int max") << QVariant(maxInt) << QJsonValue(maxInt) << QVariant(maxInt); + QTest::newRow("int min") << QVariant(minInt) << QJsonValue(minInt) << QVariant(minInt); + QTest::newRow("double pos") << QVariant(123.) << QJsonValue(123.) << QVariant(qlonglong(123.)); + QTest::newRow("double neg") << QVariant(-123.) << QJsonValue(-123.) + << QVariant(qlonglong(-123.)); + QTest::newRow("double big") << QVariant(maxDouble - 1000) << QJsonValue(maxDouble - 1000) + << QVariant(maxDouble - 1000); + QTest::newRow("double max") << QVariant(maxDouble) << QJsonValue(maxDouble) + << QVariant(maxDouble); + QTest::newRow("double min") << QVariant(minDouble) << QJsonValue(minDouble) + << QVariant(minDouble); + QTest::newRow("double big neg") << QVariant(1000 - maxDouble) << QJsonValue(1000 - maxDouble) + << QVariant(1000 - maxDouble); + QTest::newRow("double max neg") << QVariant(-maxDouble) << QJsonValue(-maxDouble) + << QVariant(-maxDouble); + QTest::newRow("double min neg") << QVariant(-minDouble) << QJsonValue(-minDouble) + << QVariant(-minDouble); + + QTest::newRow("string null") << QVariant(QString()) << QJsonValue(QString()) + << QVariant(QString()); + QTest::newRow("string empty") << QVariant(QString("")) << QJsonValue(QString("")) + << QVariant(QString("")); + QTest::newRow("string ascii") << QVariant(QString("Data")) << QJsonValue(QString("Data")) + << QVariant(QString("Data")); + QTest::newRow("string utf8") << QVariant(QString(utf8)) << QJsonValue(QString(utf8)) + << QVariant(QString(utf8)); + + QTest::newRow("bytearray null") << QVariant(QByteArray()) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("bytearray empty") << QVariant(QByteArray()) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("bytearray ascii") << QVariant(QByteArray("Data")) << QJsonValue(QString("Data")) + << QVariant(QString("Data")); + QTest::newRow("bytearray utf8") << QVariant(utf8) << QJsonValue(QString(utf8)) + << QVariant(QString(utf8)); + + QTest::newRow("datetime") << QVariant(dt) << QJsonValue(dt.toString(Qt::ISODateWithMs)) + << QVariant(dt.toString(Qt::ISODateWithMs)); + QTest::newRow("url") << QVariant(QUrl("http://example.com/{q}")) + << QJsonValue("http://example.com/%7Bq%7D") + << QVariant(QString("http://example.com/%7Bq%7D")); + QTest::newRow("uuid") << QVariant(QUuid(uuid)) + << QJsonValue(uuid.toString(QUuid::WithoutBraces)) + << QVariant(uuid.toString(QUuid::WithoutBraces)); + QTest::newRow("regexp") << QVariant(QRegularExpression(".")) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + + QTest::newRow("inf") << QVariant(qInf()) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("-inf") << QVariant(-qInf()) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); + QTest::newRow("NaN") << QVariant(qQNaN()) << QJsonValue(QJsonValue::Null) + << QVariant::fromValue(nullptr); +} + +void tst_QtJson::fromToVariantConversions() +{ + QFETCH(QVariant, variant); + QFETCH(QJsonValue, json); + QFETCH(QVariant, jsonToVariant); + + QVariant variantFromJson(json); + QVariant variantFromJsonArray(QJsonArray { json }); + QVariant variantFromJsonObject(QVariantMap { { "foo", variant } }); + + QJsonObject object { QPair("foo", json) }; + + // QJsonValue <> QVariant + { + QCOMPARE(QJsonValue::fromVariant(variant), json); + + // test the same for QVariant from QJsonValue/QJsonArray/QJsonObject + QCOMPARE(QJsonValue::fromVariant(variantFromJson), json); + QCOMPARE(QJsonValue::fromVariant(variantFromJsonArray), QJsonArray { json }); + QCOMPARE(QJsonValue::fromVariant(variantFromJsonObject), object); + + // QJsonValue to variant + QCOMPARE(json.toVariant(), jsonToVariant); + QCOMPARE(json.toVariant().userType(), jsonToVariant.userType()); + + // variant to QJsonValue + QCOMPARE(QVariant(json).toJsonValue(), json); + } + + // QJsonArray <> QVariantList + { + QCOMPARE(QJsonArray::fromVariantList(QVariantList { variant }), QJsonArray { json }); + + // test the same for QVariantList from QJsonValue/QJsonArray/QJsonObject + QCOMPARE(QJsonArray::fromVariantList(QVariantList { variantFromJson }), + QJsonArray { json }); + QCOMPARE(QJsonArray::fromVariantList(QVariantList { variantFromJsonArray }), + QJsonArray {{ QJsonArray { json } }}); + QCOMPARE(QJsonArray::fromVariantList(QVariantList { variantFromJsonObject }), + QJsonArray { object }); + + // QJsonArray to variant + QCOMPARE(QJsonArray { json }.toVariantList(), QVariantList { jsonToVariant }); + // variant to QJsonArray + QCOMPARE(QVariant(QJsonArray { json }).toJsonArray(), QJsonArray { json }); + } + + // QJsonObject <> QVariantMap + { + QCOMPARE(QJsonObject::fromVariantMap(QVariantMap { { "foo", variant } }), object); + + // test the same for QVariantMap from QJsonValue/QJsonArray/QJsonObject + QCOMPARE(QJsonObject::fromVariantMap(QVariantMap { { "foo", variantFromJson } }), object); + + QJsonObject nestedArray { QPair("bar", QJsonArray { json }) }; + QJsonObject nestedObject { QPair("bar", object) }; + QCOMPARE(QJsonObject::fromVariantMap(QVariantMap { { "bar", variantFromJsonArray } }), + nestedArray); + QCOMPARE(QJsonObject::fromVariantMap(QVariantMap { { "bar", variantFromJsonObject } }), + nestedObject); + + // QJsonObject to variant + QCOMPARE(object.toVariantMap(), QVariantMap({ { "foo", jsonToVariant } })); + // variant to QJsonObject + QCOMPARE(QVariant(object).toJsonObject(), object); + } +} + QTEST_MAIN(tst_QtJson) #include "tst_qtjson.moc"