From 107f3169bff2e4ca648283d2806c194ea826f521 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 20 May 2025 19:16:31 +0200 Subject: [PATCH] [ogr] fix JSON array interpreted as QVariantMap Fix #61728 --- src/core/providers/ogr/qgsogrprovider.cpp | 36 ++++- src/core/qgsogrutils.cpp | 1 - tests/src/core/testqgsogrprovider.cpp | 165 ++++++++++++++++++++++ 3 files changed, 199 insertions(+), 3 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 4e26aa8a67f..091bfcf93a2 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -886,9 +886,9 @@ void QgsOgrProvider::loadFields() QMutexLocker locker( datasetMutex ); #endif - for ( int i = 0; i < fdef.GetFieldCount(); ++i ) + for ( int fieldIndex = 0; fieldIndex < fdef.GetFieldCount(); ++fieldIndex ) { - OGRFieldDefnH fldDef = fdef.GetFieldDefn( i ); + OGRFieldDefnH fldDef = fdef.GetFieldDefn( fieldIndex ); const OGRFieldType ogrType = OGR_Fld_GetType( fldDef ); const OGRFieldSubType ogrSubType = OGR_Fld_GetSubType( fldDef ); @@ -896,6 +896,38 @@ void QgsOgrProvider::loadFields() QMetaType::Type varSubType = QMetaType::Type::UnknownType; QgsOgrUtils::ogrFieldTypeToQVariantType( ogrType, ogrSubType, varType, varSubType ); + // Handle special case for OGRFieldType::OFSTJSON which is not always a map. + // If subtype is JSON and varType is map, try to load a feature and check if it's + // really an object (rather than something else like an array) + // fallback to string given that only JSON object (map) is supported. + if ( varType == QMetaType::Type::QVariantMap && ogrSubType == OFSTJSON ) + { + QRecursiveMutex *layerMutex = nullptr; + OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex ); + QMutexLocker layerLocker( layerMutex ); + gdal::ogr_feature_unique_ptr f( OGR_L_GetNextFeature( ogrLayer ) ); + if ( f ) + { + const char *json = OGR_F_GetFieldAsString( f.get(), fieldIndex ); + if ( json && json[0] != '\0' ) + { + try + { + const auto json_element = json::parse( json ); + if ( ! json_element.is_object() ) + { + varType = QMetaType::Type::QString; + } + } + catch ( const json::parse_error & ) + { + varType = QMetaType::Type::QString; + } + } + OGR_L_ResetReading( ogrLayer ); + } + } + //TODO: fix this hack #ifdef ANDROID QString name = OGR_Fld_GetNameRef( fldDef ); diff --git a/src/core/qgsogrutils.cpp b/src/core/qgsogrutils.cpp index 7a7f05c05ae..3d7bec7bbee 100644 --- a/src/core/qgsogrutils.cpp +++ b/src/core/qgsogrutils.cpp @@ -1862,7 +1862,6 @@ void QgsOgrUtils::ogrFieldTypeToQVariantType( OGRFieldType ogrType, OGRFieldSubT case OFTWideString: if ( ogrSubType == OFSTJSON ) { - ogrSubType = OFSTJSON; variantType = QMetaType::Type::QVariantMap; variantSubType = QMetaType::Type::QString; } diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 56e5fc427e3..34fdace65cd 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -58,6 +58,8 @@ class TestQgsOgrProvider : public QgsTest void testExtent(); void testVsiCredentialOptions(); void testVsiCredentialOptionsQuerySublayers(); + void testJSONFields_data(); + void testJSONFields(); private: QString mTestDataDir; @@ -579,5 +581,168 @@ void TestQgsOgrProvider::testVsiCredentialOptionsQuerySublayers() } +void TestQgsOgrProvider::testJSONFields_data() +{ + QTest::addColumn( "jsonData" ); + QTest::addColumn( "expectedType" ); + + QTest::newRow( "array of map" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "array_of_map": [ + { + "a": 1, + "b": 2.0 + } + ] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QString ); + + QTest::newRow( "map" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "map": { + "a": 1, + "b": 2.0 + } + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantMap ); + + QTest::newRow( "int" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "int": 1 + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Int ); + + QTest::newRow( "stringlist" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "string_list": [ "a", "b", "c" ] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QStringList ); + + QTest::newRow( "string" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "string": "a" + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QString ); + + QTest::newRow( "double" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "double": 1.0 + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Double ); + + QTest::newRow( "bool" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "bool": true + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Bool ); + + QTest::newRow( "int list" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "int_list": [1, 2, 3] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantList ); + + QTest::newRow( "real list" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "real_list": [1.0, 2.1, 3] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantList ); +} + +void TestQgsOgrProvider::testJSONFields() +{ + QFETCH( QString, jsonData ); + QFETCH( int, expectedType ); + + QTemporaryDir dir; + QString filePath = dir.path() + "/test.json"; + QFile file( filePath ); + if ( file.open( QIODevice::WriteOnly ) ) + { + QTextStream textStream( &file ); + textStream << jsonData; + file.close(); + } + QgsVectorLayer layer { filePath, QStringLiteral( "json" ), QLatin1String( "ogr" ) }; + QVERIFY( layer.isValid() ); + QgsFields fields = layer.fields(); + QCOMPARE( fields.count(), 1 ); + QgsField field = fields.at( 0 ); + QCOMPARE( static_cast( field.type() ), expectedType ); +} + + QGSTEST_MAIN( TestQgsOgrProvider ) #include "testqgsogrprovider.moc"