有没有一种理智的方法可以将具有未知结构的嵌套 JSON 解析为 Swift 5 中的对象或字典?

And*_*Mac 0 json ios swift swift5

免责声明:发布了很多类似的问题,但似乎没有一个与我自己的问题重复

假设我的应用程序从设计非常糟糕的 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"
                }
            ]
        }]
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我想从所有比赛中获得所有兴趣,在原生 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
}
Run Code Online (Sandbox Code Playgroud)

然后,我必须以如下方式使用我创建的结构:

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
        }
}
Run Code Online (Sandbox Code Playgroud)

虽然这有点儿工作,它有两个主要缺点。

首先,对于一个如此简单的任务,所有这些放在一起是大量的准备工作和代码,它不符合 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
  })
}
Run Code Online (Sandbox Code Playgroud)

蟒蛇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
Run Code Online (Sandbox Code Playgroud)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

所以,问题是,我如何以理智的方式在 Swift 中实现相同的目标,就像在上面的示例中一样(这绝对意味着大致相同数量的代码)?如果您知道这样做的第 3 方库,我很乐意接受这样的库作为答案。

更新:刚刚发现Jsonify,它似乎至少与SwiftyJSON一样是解决我问题的好方法,甚至可能更好。人们可能应该尝试一段时间来决定哪一种更适合他们的口味并且需要更好

mat*_*att 6

首先,请记住,结构不需要在范围内是全局的。您可以在函数中“临时”定义一个结构体,只是为了帮助您深入了解 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)
Run Code Online (Sandbox Code Playgroud)

好的,现在我们已经解析了该死的数据。但我们讨厌它的结构。所以改变它!

// 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 }
Run Code Online (Sandbox Code Playgroud)

但是我们仍然需要处理愚蠢的单元素数组:

// okay, but the one-element array is silly, so flatten that too
let persons5 = persons4.map {$0.first!}
Run Code Online (Sandbox Code Playgroud)

现在你说你根本不关心这个人的事情,你想要的只是兴趣列表:

// okay but all I care about are the interests
let interests = persons5.map {$0.interests}
Run Code Online (Sandbox Code Playgroud)

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

现在,如果所有这些都在一个方法中完成,那么只有兴趣结构需要是公共的;其他一切只是提取值的工具,并且可以是方法内部私有的。只公开您的应用程序真正需要/想要维护数据的结构。不过,总的教训是:只需将 JSON 解析为对象:现在您处于 Swift 对象世界中,并且可以以最终对您有用的任何方式重新解析这些对象。

  • 是的,如果您不想依赖第三方库,您可以使用 JSONSerialization 做同样的事情。但我们很少对传入的 JSON 知之甚少,以至于需要这样做;我不能同意接受的答案,除非你的 JSON 是完全随机的。 (2认同)