簡體   English   中英

有沒有一種理智的方法可以將具有未知結構的嵌套 JSON 解析為 Swift 5 中的對象或字典?

[英]Is there a sane way to parse a nested JSON with unknown structure into an object or a dictionary in Swift 5?

免責聲明:發布了很多類似的問題,但似乎沒有一個與我自己的問題重復

假設我的應用程序從設計非常糟糕的 API 接收到一個 JSON,並且該 JSON 看起來像這樣:

{
    "matches": {
        "page1": [{
            "name": "John",
            "surname": "Doe",
            "interests": [{
                    "id": 13,
                    "text": "basketball"
                },
                {
                    "id": 37,
                    "text": "competitive knitting"
                },
                {
                    "id": 127,
                    "text": "romcoms"
                }
            ]
        }],
        "page2": [{
            "name": "Dwayne",
            "surname": "Johnson",
            "interests": [{
                    "id": 42,
                    "text": "sci-fi"
                },
                {
                    "id": 255,
                    "text": "round numbers"
                }
            ]
        }]
    }
}

如果我想從所有比賽中獲得所有興趣,在原生 Swift 功能中,我首先必須這樣做:

struct MatchesData: Codable {
    let matches: Matches
}

struct Matches: Codable {
    let page1: Page1
    let page2: Page2
}

struct Page1: Codable {
    let interests: [Interest]
}

struct Page2: Codable {
    let interests: [Interest]
}

struct Interest: Codable {
    let id: Int
    let text: String
}

然后,我必須以如下方式使用我創建的結構:

func handleJSON(_ response: Data) {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(MatchesData.self, from: response)
            // Only here I can start actually working with the data API sent me, it's in decodedData
            ...
        } catch {
            // Handle the errors somehow
            return
        }
}

雖然這有點兒工作,它有兩個主要缺點。

首先,對於一個如此簡單的任務,所有這些放在一起是大量的准備工作和代碼,它不符合 DRY 原則。

最后,如果您事先不知道 JSON 的確切結構,這種方法根本不起作用。 例如,在我的示例中,如果頁面數量沒有固定為 2,而是可能介於 1 和 50 之間,該怎么辦?

在其他具有用於處理 JSON 的健全的內置工具的語言中,我只需解析 JSON 並遞歸地遍歷匹配項,以對內容執行我需要的操作。

示例(沒有提高可讀性的安全預防措施)

JS:

const handleJSON = jsonStr => {
  const jsonObj = JSON.parse(jsonStr)
  const matches = jsonObj.matches
  Object.values(matches).forEach(page => {
    // Recursively process every page to find what I need and process it
  })
}

蟒蛇3:

import json

def handleJSON(jsonStr):
  jsonObj = json.loads(jsonStr)
  matches = jsonObj['matches']
  for page in matches:
    # Recursively process every page to find what I need and process it

PHP:

function handleJSON($jsonStr) {
    $jsonObj = json_decode($jsonStr);
    $matches = $jsonObj->matches;
    foreach ($matches as $page) {
        // Recursively process every page to find what I need and process it
    }
}

所以,問題是,我如何以理智的方式在 Swift 中實現相同的目標,就像上面的例子一樣(這絕對意味着大致相同的代碼量)? 如果您知道這樣做的第 3 方庫,我很樂意接受這樣的庫作為答案。

更新:剛剛發現Jsonify ,它似乎至少與SwiftyJSON一樣是解決我問題的好方法,甚至可能更好。 人們可能應該嘗試一段時間來決定哪一種更適合他們的口味並且需要更好

首先,請記住,結構不需要在范圍內是全局的。 您可以在函數中“臨時”定義一個結構體,只是為了幫助您深入了解 JSON 並提取一個值。 因此,代碼的整體架構絕不會受到損害。

其次,您自己的示例 JSON 並不像您建議的那樣瘋狂; 瘋狂的是你自己的結構。 首先,您使用了兩個相同的結構。 擁有結構 Page1 和結構 Page2 沒有任何意義。 其次,你的比賽也沒有任何目的。 那不是你的 JSON 是什么。 在您的 MatchesData 中, matches屬性應該只是一個[String:[Person]]類型的字典(您沒有 Person 類型,但這似乎是這些東西)。

如果您聲稱字典應該是 JSON 中的數組,那很好,我同意。 如果鍵總是被命名為"page1""page2"等,那么 JSON 在這方面是愚蠢的。 但是,您可以稍后再變換[String:[Person]]在字符串的結尾到人的數組排序由數字"page"鍵。 然后,如果您想丟棄其余的人員信息,則可以將其映射到一個興趣數組中。

換句話說:您如何解析到達您的數據以及您如何維護您感興趣的數據,是兩個完全不同的問題。 解析數據只是讓自己脫離 JSON 世界,進入對象世界; 然后將其轉化為對您有用的形式,保留您需要的並忽略您不需要的。

以下是您可能執行的操作的示例:

let data = s.data(using: .utf8)!

struct Result : Codable {
    let matches:[String:[Person]]
}
struct Person : Codable {
    let name:String
    let surname:String
    let interests:[Interests]
}
struct Interests : Codable {
    let id:Int
    let text:String
}
let result = try! JSONDecoder().decode(Result.self, from: data)

好的,現在我們已經解析了該死的數據。 但我們討厭它的結構。 所以改變它!

// okay, but dictionary is silly: flatten to an array
let persons1 = Array(result.matches)
func pageNameToNumber(_ s:String) -> Int {
    let num = s.dropFirst(4)
    return Int(num)!
}
let persons2 = persons1.map {(key:pageNameToNumber($0.key), value:$0.value)}
let persons3 = persons2.sorted {$0.key < $1.key}
let persons4 = persons3.map { $0.value }

但是我們仍然需要處理愚蠢的單元素數組:

// okay, but the one-element array is silly, so flatten that too
let persons5 = persons4.map {$0.first!}

現在你說你根本不關心這個人的事情,你想要的只是興趣列表:

// okay but all I care about are the interests
let interests = persons5.map {$0.interests}

(然后你也可以扁平化興趣,或者其他什么。)

現在,如果所有這些都在一個方法中完成,那么只有興趣結構需要是公共的; 其他一切只是提取值的工具,並且可以是方法內部私有的。 只公開您的應用程序真正需要/想要維護數據的結構。 不過,總體教訓是:只需將 JSON 解析為對象:現在您處於 Swift 對象世界中,並且可以以最終對您有用的任何方式重新解析這些對象。

如果您正在尋找執行此操作的外部庫,您可以嘗試SwiftyJSON ,它可以讓您獲得所有這樣的興趣:

    let json = JSON(parseJSON: jsonString)
    let matchesObject = json["matches"]
    // I assume the total number of pages is stored in numberOfPages
    let interests = (0..<2).flatMap {
        matchesObject["page\($0 + 1)"].arrayValue.flatMap {
            $0["interests"].arrayValue.map {
                // using the same Interest struct as in your question
                Interest(id: $0["id"].intValue, text: $0["text"].stringValue)
            }
        }
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM