Handle special cases of JSON serializing of the Well-known types

Change the way we handle them. Add the special registry so any
type can register its special JSON (de)serializer independently.

Move the timestamp serializer from the generic JSON serializer
implementation to QtProtobufWellknownTypes library so we remove
the weak backward link from QtProtobuf module to its dependency.

The introduced mechanism is also scalable, and allows adding other
types that have similar special JSON serialization.

Task-number: QTBUG-130555
Task-number: QTBUG-120214
Change-Id: I56ce2e43a00262069281871d5f903f1e94abef83
Reviewed-by: Dennis Oberst <dennis.oberst@qt.io>
This commit is contained in:
Alexey Edelev 2025-01-10 18:04:59 +01:00
parent 0dba8755c8
commit 00a86d9d2c
8 changed files with 242 additions and 77 deletions

View File

@ -9,7 +9,7 @@ qt_internal_add_module(Protobuf
qtprotobufglobal.h
qabstractprotobufserializer.cpp qabstractprotobufserializer.h
qprotobufdeserializerbase_p.h qprotobufdeserializerbase.cpp
qprotobufjsonserializer.cpp qprotobufjsonserializer.h
qprotobufjsonserializer.cpp qprotobufjsonserializer.h qprotobufjsonserializer_p.h
qprotobuflazymessagepointer.h
qprotobufmessage.cpp qprotobufmessage.h qprotobufmessage_p.h
qprotobufobject.h

View File

@ -6,6 +6,7 @@
#include <QtProtobuf/private/protobuffieldpresencechecker_p.h>
#include <QtProtobuf/private/protobufscalarjsonserializers_p.h>
#include <QtProtobuf/private/qprotobufdeserializerbase_p.h>
#include <QtProtobuf/private/qprotobufjsonserializer_p.h>
#include <QtProtobuf/private/qprotobufregistration_p.h>
#include <QtProtobuf/private/qprotobufserializerbase_p.h>
#include <QtProtobuf/private/qtprotobufdefs_p.h>
@ -17,6 +18,7 @@
#include <QtCore/qjsonarray.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qjsonobject.h>
#include <QtCore/qreadwritelock.h>
#include <QtCore/qtimezone.h>
#include <QtCore/qvariant.h>
@ -45,6 +47,41 @@ using namespace ProtobufScalarJsonSerializers;
namespace {
struct JsonHandlerRegistry
{
void registerHandler(QMetaType metaType, QtProtobufPrivate::CustomJsonSerializer serializer,
QtProtobufPrivate::CustomJsonDeserializer deserializer)
{
QWriteLocker locker(&m_lock);
m_registry[metaType] = { serializer, deserializer };
}
QtProtobufPrivate::CustomJsonSerializer findSerializer(QMetaType metaType)
{
QReadLocker locker(&m_lock);
const auto it = m_registry.constFind(metaType);
if (it != m_registry.constEnd())
return it.value().first;
return nullptr;
}
QtProtobufPrivate::CustomJsonDeserializer findDeserializer(QMetaType metaType)
{
QReadLocker locker(&m_lock);
const auto it = m_registry.constFind(metaType);
if (it != m_registry.constEnd())
return it.value().second;
return nullptr;
}
private:
using Handler = std::pair<QtProtobufPrivate::CustomJsonSerializer,
QtProtobufPrivate::CustomJsonDeserializer>;
QReadWriteLock m_lock;
QHash<QMetaType, Handler> m_registry;
};
Q_GLOBAL_STATIC(JsonHandlerRegistry, jsonHandlersRegistry)
inline QString convertJsonKeyToJsonName(QStringView name)
{
QString result;
@ -63,6 +100,27 @@ inline QString convertJsonKeyToJsonName(QStringView name)
}
void QtProtobufPrivate::registerCustomJsonHandler(QMetaType metaType,
QtProtobufPrivate::CustomJsonSerializer
serializer,
QtProtobufPrivate::CustomJsonDeserializer
deserializer)
{
jsonHandlersRegistry->registerHandler(metaType, serializer, deserializer);
}
QtProtobufPrivate::CustomJsonSerializer
QtProtobufPrivate::findCustomJsonSerializer(QMetaType metaType)
{
return jsonHandlersRegistry->findSerializer(metaType);
}
QtProtobufPrivate::CustomJsonDeserializer
QtProtobufPrivate::findCustomJsonDeserializer(QMetaType metaType)
{
return jsonHandlersRegistry->findDeserializer(metaType);
}
class QProtobufJsonSerializerImpl final : public QProtobufSerializerBase
{
public:
@ -84,9 +142,6 @@ private:
void serializeMessageFieldEnd(const QProtobufMessage *message,
const QProtobufFieldInfo &fieldInfo) override;
void serializeTimestamp(const QProtobufMessage *message,
const QtProtobufPrivate::QProtobufFieldInfo &fieldInfo);
QJsonObject m_result;
QList<QJsonObject> m_state;
@ -115,8 +170,6 @@ private:
int nextFieldIndex(QProtobufMessage *message) override;
bool deserializeScalarField(QVariant &, const QtProtobufPrivate::QProtobufFieldInfo &) override;
[[nodiscard]] bool deserializeTimestamp(QProtobufMessage *message);
struct JsonDeserializerState
{
JsonDeserializerState(const QJsonObject &obj) : obj(obj) { }
@ -194,43 +247,19 @@ void QProtobufJsonSerializerImpl::serializeMessageField(const QProtobufMessage *
const QtProtobufPrivate::QProtobufFieldInfo
&fieldInfo)
{
if (message->propertyOrdering()
->messageFullName()
.compare(QString::fromUtf8("google.protobuf.Timestamp"))
== 0) {
serializeTimestamp(message, fieldInfo);
if (!message)
return;
const auto *metaObject = QtProtobufSerializerHelpers::messageMetaObject(message);
if (auto *serializer = QtProtobufPrivate::findCustomJsonSerializer(metaObject->metaType())) {
if (const QJsonValue value = serializer(message); !value.isUndefined())
m_result.insert(fieldInfo.jsonName().toString(), value);
} else {
QProtobufSerializerBase::serializeMessageField(message, fieldInfo);
}
}
void QProtobufJsonSerializerImpl::serializeTimestamp(const QProtobufMessage *message,
const QtProtobufPrivate::QProtobufFieldInfo
&fieldInfo)
{
qint64 secs = 0;
qint32 nanos = 0;
if (const auto secondsValue = message->property("seconds"); secondsValue.canConvert<qint64>()) {
secs = secondsValue.value<qint64>();
} else {
qWarning() << "QProtobufJsonSerializerImpl::serializeTimestamp() failed to convert seconds";
}
if (const auto nanosValue = message->property("nanos"); nanosValue.canConvert<qint32>()) {
nanos = nanosValue.value<qint32>();
} else {
qWarning() << "QProtobufJsonSerializerImpl::serializeTimestamp() failed to convert nanos";
}
const auto datetime = QDateTime::fromMSecsSinceEpoch(secs * 1000 + nanos / 1000000,
QTimeZone::UTC);
const auto datetimeISO = datetime.toString(Qt::ISODateWithMs);
const auto jsonName = fieldInfo.jsonName();
m_result.insert(jsonName.toString(), serializeCommon<QString>(datetimeISO));
}
bool QProtobufJsonSerializerImpl::serializeEnum(QVariant &value,
const QProtobufFieldInfo &fieldInfo)
@ -328,13 +357,15 @@ void QProtobufJsonDeserializerImpl::setError(QAbstractProtobufSerializer::Error
bool QProtobufJsonDeserializerImpl::deserializeMessageField(QProtobufMessage *message)
{
if (!m_state.last().scalarValue.isNull()) {
if (message->propertyOrdering()
->messageFullName()
.compare(QString::fromUtf8("google.protobuf.Timestamp"))
== 0
&& m_state.last().scalarValue.isString()) {
return deserializeTimestamp(message);
if (!message)
return true;
const auto &value = m_state.last().scalarValue;
if (!value.isNull()) {
const auto *metaObject = QtProtobufSerializerHelpers::messageMetaObject(message);
if (auto *deserializer = QtProtobufPrivate::findCustomJsonDeserializer(metaObject
->metaType())) {
return deserializer(message, value);
}
setInvalidFormatError();
return false;
@ -342,37 +373,6 @@ bool QProtobufJsonDeserializerImpl::deserializeMessageField(QProtobufMessage *me
return QProtobufDeserializerBase::deserializeMessageField(message);
}
bool QProtobufJsonDeserializerImpl::deserializeTimestamp(QProtobufMessage *message)
{
const auto tsString = m_state.last().scalarValue.toString();
// Protobuf requires upper-case letters in timestamp string be case sensitive.
if (tsString.toUpper() != tsString)
return false;
if (tsString.contains(u' '))
return false;
//Ensure the field either ends with Z or a valid offset
static const QRegularExpression TimeStampEnding(".+([\\+\\-]\\d{2}:\\d{2}|Z)$"_L1);
if (!TimeStampEnding.match(tsString).hasMatch())
return false;
const auto datetime = QDateTime::fromString(tsString, Qt::ISODateWithMs);
if (!datetime.isValid()) {
qWarning() << "QProtobufJsonDeserializerImpl::deserializeTimestamp() datetime is invalid";
return false;
}
const auto msecs = datetime.toMSecsSinceEpoch();
const qint64 seconds = msecs / 1000;
const qint32 nanos = (msecs % 1000) * 1000000;
message->setProperty("seconds", QVariant::fromValue(seconds));
message->setProperty("nanos", QVariant::fromValue(nanos));
return true;
}
bool QProtobufJsonDeserializerImpl::deserializeEnum(QVariant &value,
const QtProtobufPrivate::QProtobufFieldInfo
&fieldInfo)

View File

@ -0,0 +1,45 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QPROTOBUFJSONSERIALIZER_P_H
#define QPROTOBUFJSONSERIALIZER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtProtobuf/qtprotobufexports.h>
#include <QtCore/qtconfigmacros.h>
QT_BEGIN_NAMESPACE
class QProtobufMessage;
class QJsonValue;
class QMetaType;
namespace QtProtobufPrivate {
struct QProtobufFieldInfo;
using CustomJsonSerializer = QJsonValue (*)(const QProtobufMessage *);
using CustomJsonDeserializer = bool (*)(QProtobufMessage *, const QJsonValue &);
Q_PROTOBUF_EXPORT void registerCustomJsonHandler(QMetaType metaType,
CustomJsonSerializer serializer,
CustomJsonDeserializer deserializer);
[[nodiscard]] CustomJsonSerializer findCustomJsonSerializer(QMetaType metaType);
[[nodiscard]] CustomJsonDeserializer findCustomJsonDeserializer(QMetaType metaType);
}
QT_END_NAMESPACE
#endif // QPROTOBUFJSONSERIALIZER_P_H

View File

@ -149,6 +149,11 @@ void MessageDefinitionPrinter::printRegisterBody()
if (m_descriptor->full_name() == "google.protobuf.Any")
m_printer->Print("QT_PREPEND_NAMESPACE(QtProtobuf)::Any::registerTypes();\n");
if (m_descriptor->full_name() == "google.protobuf.Timestamp") {
m_printer->Print("QT_PREPEND_NAMESPACE(QtProtobufWellKnownTypesPrivate)::"
"registerTimestampCustomJsonHandler();\n");
}
common::iterateMessageFields(
m_descriptor, [&](const FieldDescriptor *field, const PropertyMap &propertyMap) {
auto it = propertyMap.find("full_type");

View File

@ -73,8 +73,11 @@ void QProtobufGenerator::GenerateSources(const FileDescriptor *file,
return;
}
});
if (generateWellknownTimestamp)
if (generateWellknownTimestamp) {
externalIncludes
.insert("QtProtobufWellKnownTypes/private/qprotobufwellknowntypesjsonserializers_p.h");
externalIncludes.insert("QtCore/QTimeZone");
}
printIncludes(sourcePrinter.get(), internalIncludes, externalIncludes, { "cmath" });
OpenFileNamespaces(file, sourcePrinter.get());

View File

@ -5,6 +5,7 @@ qt_internal_add_protobuf_module(ProtobufWellKnownTypes
SOURCES
qtprotobufwellknowntypesglobal.h
qprotobufanysupport.cpp qprotobufanysupport.h
qprotobufwellknowntypesjsonserializers_p.h qprotobufwellknowntypesjsonserializers.cpp
PUBLIC_LIBRARIES
Qt::Protobuf
LIBRARIES

View File

@ -0,0 +1,83 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QtProtobufWellKnownTypes/private/qprotobufwellknowntypesjsonserializers_p.h>
#include <QtProtobufWellKnownTypes/timestamp.qpb.h>
#include <QtProtobuf/private/protobufscalarjsonserializers_p.h>
#include <QtProtobuf/private/qprotobufjsonserializer_p.h>
#include <QtCore/qjsonvalue.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
namespace {
QJsonValue serializeProtobufWellKnownTimestamp(const QProtobufMessage *message)
{
qint64 secs = 0;
qint32 nanos = 0;
if (const auto secondsValue = message->property("seconds"); secondsValue.canConvert<qint64>()) {
secs = secondsValue.value<qint64>();
} else {
qWarning() << "serializeTimestamp() failed to convert seconds";
}
if (const auto nanosValue = message->property("nanos"); nanosValue.canConvert<qint32>()) {
nanos = nanosValue.value<qint32>();
} else {
qWarning() << "serializeTimestamp() failed to convert nanos";
}
const auto datetime = QDateTime::fromMSecsSinceEpoch(secs * 1000 + nanos / 1000000,
QTimeZone::UTC);
return ProtobufScalarJsonSerializers::serializeCommon<
QString>(datetime.toString(Qt::ISODateWithMs));
}
bool deserializeProtobufWellKnownTimestamp(QProtobufMessage *message, const QJsonValue &value)
{
if (!value.isString())
return false;
const auto tsString = value.toString();
// Protobuf requires upper-case letters in timestamp string be case sensitive.
if (tsString.toUpper() != tsString)
return false;
if (tsString.contains(u' '))
return false;
//Ensure the field either ends with Z or a valid offset
static const QRegularExpression TimeStampEnding(".+([\\+\\-]\\d{2}:\\d{2}|Z)$"_L1);
if (!TimeStampEnding.match(tsString).hasMatch())
return false;
const auto datetime = QDateTime::fromString(tsString, Qt::ISODateWithMs);
if (!datetime.isValid()) {
qWarning() << "deserializeTimestamp() datetime is invalid";
return false;
}
const auto msecs = datetime.toMSecsSinceEpoch();
const qint64 seconds = msecs / 1000;
const qint32 nanos = (msecs % 1000) * 1000000;
message->setProperty("seconds", QVariant::fromValue(seconds));
message->setProperty("nanos", QVariant::fromValue(nanos));
return true;
}
} // namespace
void QtProtobufWellKnownTypesPrivate::registerTimestampCustomJsonHandler()
{
QtProtobufPrivate::registerCustomJsonHandler(QMetaType::fromType<google::protobuf::Timestamp>(),
serializeProtobufWellKnownTimestamp,
deserializeProtobufWellKnownTimestamp);
}
QT_END_NAMESPACE

View File

@ -0,0 +1,28 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H
#define QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtCore/qtconfigmacros.h>
QT_BEGIN_NAMESPACE
namespace QtProtobufWellKnownTypesPrivate {
void registerTimestampCustomJsonHandler();
}
QT_END_NAMESPACE
#endif // QPROTOBUFWELLKNOWNTYPESJSONSERIALIZERS_P_H