如何使用 Swift Decodable 协议解码嵌套的 JSON 结构?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/44549310/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 18:55:30  来源:igfitidea点击:

How to decode a nested JSON struct with Swift Decodable protocol?

jsonswiftswift4codable

提问by FlowUI. SimpleUITesting.com

Here is my JSON

这是我的 JSON

{
    "id": 1,
    "user": {
        "user_name": "Tester",
        "real_info": {
            "full_name":"Jon Doe"
        }
    },
    "reviews_count": [
        {
            "count": 4
        }
    ]
}

Here is the structure I want it saved to (incomplete)

这是我希望它保存到的结构(不完整)

struct ServerResponse: Decodable {
    var id: String
    var username: String
    var fullName: String
    var reviewCount: Int

    enum CodingKeys: String, CodingKey {
       case id, 
       // How do i get nested values?
    }
}

I have looked at Apple's Documentationon decoding nested structs, but I still do not understand how to do the different levels of the JSON properly. Any help will be much appreciated.

我已经查看了Apple关于解码嵌套结构的文档,但我仍然不明白如何正确处理不同级别的 JSON。任何帮助都感激不尽。

回答by Code Different

Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:

另一种方法是创建一个与 JSON 紧密匹配的中间模型(借助quicktype.io之类的工具),让 Swift 生成解码它的方法,然后在最终数据模型中挑选出您想要的部分:

// snake_case to match the JSON and hence no need to write CodingKey enums / struct
fileprivate struct RawServerResponse: Decodable {
    struct User: Decodable {
        var user_name: String
        var real_info: UserRealInfo
    }

    struct UserRealInfo: Decodable {
        var full_name: String
    }

    struct Review: Decodable {
        var count: Int
    }

    var id: Int
    var user: User
    var reviews_count: [Review]
}

struct ServerResponse: Decodable {
    var id: String
    var username: String
    var fullName: String
    var reviewCount: Int

    init(from decoder: Decoder) throws {
        let rawResponse = try RawServerResponse(from: decoder)

        // Now you can pick items that are important to your data model,
        // conveniently decoded into a Swift structure
        id = String(rawResponse.id)
        username = rawResponse.user.user_name
        fullName = rawResponse.user.real_info.full_name
        reviewCount = rawResponse.reviews_count.first!.count
    }
}

This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.

如果reviews_count将来包含 1 个以上的值,这也允许您轻松迭代。

回答by Imanou Petit

In order to solve your problem, you can split your RawServerResponseimplementation into several logic parts (using Swift 5).

为了解决您的问题,您可以将您的RawServerResponse实现分成几个逻辑部分(使用 Swift 5)。



#1. Implement the properties and required coding keys

#1. 实现属性和所需的编码键

import Foundation

struct RawServerResponse {

    enum RootKeys: String, CodingKey {
        case id, user, reviewCount = "reviews_count"
    }

    enum UserKeys: String, CodingKey {
        case userName = "user_name", realInfo = "real_info"
    }

    enum RealInfoKeys: String, CodingKey {
        case fullName = "full_name"
    }

    enum ReviewCountKeys: String, CodingKey {
        case count
    }

    let id: Int
    let userName: String
    let fullName: String
    let reviewCount: Int

}

#2. Set the decoding strategy for idproperty

#2. 设置id属性的解码策略

extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        // id
        let container = try decoder.container(keyedBy: RootKeys.self)
        id = try container.decode(Int.self, forKey: .id)

        /* ... */                 
    }

}

#3. Set the decoding strategy for userNameproperty

#3. 设置userName属性的解码策略

extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        /* ... */

        // userName
        let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
        userName = try userContainer.decode(String.self, forKey: .userName)

        /* ... */
    }

}

#4. Set the decoding strategy for fullNameproperty

#4. 设置fullName属性的解码策略

extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        /* ... */

        // fullName
        let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
        fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)

        /* ... */
    }

}

#5. Set the decoding strategy for reviewCountproperty

#5. 设置reviewCount属性的解码策略

extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        /* ...*/        

        // reviewCount
        var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
        var reviewCountArray = [Int]()
        while !reviewUnkeyedContainer.isAtEnd {
            let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
            reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
        }
        guard let reviewCount = reviewCountArray.first else {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
        }
        self.reviewCount = reviewCount
    }

}


Complete implementation

完成实施

import Foundation

struct RawServerResponse {

    enum RootKeys: String, CodingKey {
        case id, user, reviewCount = "reviews_count"
    }

    enum UserKeys: String, CodingKey {
        case userName = "user_name", realInfo = "real_info"
    }

    enum RealInfoKeys: String, CodingKey {
        case fullName = "full_name"
    }

    enum ReviewCountKeys: String, CodingKey {
        case count
    }

    let id: Int
    let userName: String
    let fullName: String
    let reviewCount: Int

}
extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        // id
        let container = try decoder.container(keyedBy: RootKeys.self)
        id = try container.decode(Int.self, forKey: .id)

        // userName
        let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
        userName = try userContainer.decode(String.self, forKey: .userName)

        // fullName
        let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
        fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)

        // reviewCount
        var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
        var reviewCountArray = [Int]()
        while !reviewUnkeyedContainer.isAtEnd {
            let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
            reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
        }
        guard let reviewCount = reviewCountArray.first else {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
        }
        self.reviewCount = reviewCount
    }

}

Usage

用法

let jsonString = """
{
    "id": 1,
    "user": {
        "user_name": "Tester",
        "real_info": {
            "full_name":"Jon Doe"
        }
    },
    "reviews_count": [
    {
    "count": 4
    }
    ]
}
"""

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let serverResponse = try! decoder.decode(RawServerResponse.self, from: jsonData)
dump(serverResponse)

/*
prints:
? RawServerResponse #1 in __lldb_expr_389
  - id: 1
  - user: "Tester"
  - fullName: "Jon Doe"
  - reviewCount: 4
*/

回答by Hamish

Rather than having one big CodingKeysenumeration with allthe keys you'll need for decoding the JSON, I would advise splitting the keys up for eachof your nested JSON objects, using nested enumerations to preserve the hierarchy:

与其使用包含解码 JSON 所需的所有键的大CodingKeys枚举,我建议将每个嵌套 JSON 对象的键分开,使用嵌套枚举来保留层次结构:

// top-level JSON object keys
private enum CodingKeys : String, CodingKey {

    // using camelCase case names, with snake_case raw values where necessary.
    // the raw values are what's used as the actual keys for the JSON object,
    // and default to the case name unless otherwise specified.
    case id, user, reviewsCount = "reviews_count"

    // "user" JSON object keys
    enum User : String, CodingKey {
        case username = "user_name", realInfo = "real_info"

        // "real_info" JSON object keys
        enum RealInfo : String, CodingKey {
            case fullName = "full_name"
        }
    }

    // nested JSON objects in "reviews" keys
    enum ReviewsCount : String, CodingKey {
        case count
    }
}

This will make it easier to keep track of the keys at each level in your JSON.

这样可以更轻松地跟踪 JSON 中每个级别的键。

Now, bearing in mind that:

现在,请记住:

  • A keyed containeris used to decode a JSON object, and is decoded with a CodingKeyconforming type (such as the ones we've defined above).

  • An unkeyed containeris used to decode a JSON array, and is decoded sequentially(i.e each time you call a decode or nested container method on it, it advances to the next element in the array). See the second part of the answer for how you can iterate through one.

  • 键控容器用于解码JSON对象,并且被解码以CodingKey符合类型(如那些我们已经定义如上)。

  • 键容器用于解码 JSON 数组,并按顺序解码(即每次调用 decode 或嵌套容器方法时,它都会前进到数组中的下一个元素)。请参阅答案的第二部分,了解如何遍历一个。

After getting your top-level keyedcontainer from the decoder with container(keyedBy:)(as you have a JSON object at the top-level), you can repeatedly use the methods:

从解码器获取顶级键控容器后container(keyedBy:)(因为您在顶级有一个 JSON 对象),您可以重复使用这些方法:

For example:

例如:

struct ServerResponse : Decodable {

    var id: Int, username: String, fullName: String, reviewCount: Int

    private enum CodingKeys : String, CodingKey { /* see above definition in answer */ }

    init(from decoder: Decoder) throws {

        // top-level container
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)

        // container for { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
        let userContainer =
            try container.nestedContainer(keyedBy: CodingKeys.User.self, forKey: .user)

        self.username = try userContainer.decode(String.self, forKey: .username)

        // container for { "full_name": "Jon Doe" }
        let realInfoContainer =
            try userContainer.nestedContainer(keyedBy: CodingKeys.User.RealInfo.self,
                                              forKey: .realInfo)

        self.fullName = try realInfoContainer.decode(String.self, forKey: .fullName)

        // container for [{ "count": 4 }] – must be a var, as calling a nested container
        // method on it advances it to the next element.
        var reviewCountContainer =
            try container.nestedUnkeyedContainer(forKey: .reviewsCount)

        // container for { "count" : 4 }
        // (note that we're only considering the first element of the array)
        let firstReviewCountContainer =
            try reviewCountContainer.nestedContainer(keyedBy: CodingKeys.ReviewsCount.self)

        self.reviewCount = try firstReviewCountContainer.decode(Int.self, forKey: .count)
    }
}

Example decoding:

示例解码:

let jsonData = """
{
  "id": 1,
  "user": {
    "user_name": "Tester",
    "real_info": {
    "full_name":"Jon Doe"
  }
  },
  "reviews_count": [
    {
      "count": 4
    }
  ]
}
""".data(using: .utf8)!

do {
    let response = try JSONDecoder().decode(ServerResponse.self, from: jsonData)
    print(response)
} catch {
    print(error)
}

// ServerResponse(id: 1, username: "Tester", fullName: "Jon Doe", reviewCount: 4)


Iterating through an unkeyed container

遍历无键容器

Considering the case where you want reviewCountto be an [Int], where each element represents the value for the "count"key in the nested JSON:

考虑到您想要reviewCount成为 的情况[Int],其中每个元素代表"count"嵌套 JSON 中键的值:

  "reviews_count": [
    {
      "count": 4
    },
    {
      "count": 5
    }
  ]

You'll need to iterate through the nested unkeyed container, getting the nested keyed container at each iteration, and decoding the value for the "count"key. You can use the countproperty of the unkeyed container in order to pre-allocate the resultant array, and then the isAtEndproperty to iterate through it.

您需要遍历嵌套的无键容器,在每次迭代时获取嵌套的有键容器,并解码"count"键的值。您可以使用countunkeyed 容器的属性来预分配结果数组,然后使用该isAtEnd属性迭代它。

For example:

例如:

struct ServerResponse : Decodable {

    var id: Int
    var username: String
    var fullName: String
    var reviewCounts = [Int]()

    // ...

    init(from decoder: Decoder) throws {

        // ...

        // container for [{ "count": 4 }, { "count": 5 }]
        var reviewCountContainer =
            try container.nestedUnkeyedContainer(forKey: .reviewsCount)

        // pre-allocate the reviewCounts array if we can
        if let count = reviewCountContainer.count {
            self.reviewCounts.reserveCapacity(count)
        }

        // iterate through each of the nested keyed containers, getting the
        // value for the "count" key, and appending to the array.
        while !reviewCountContainer.isAtEnd {

            // container for a single nested object in the array, e.g { "count": 4 }
            let nestedReviewCountContainer = try reviewCountContainer.nestedContainer(
                                                 keyedBy: CodingKeys.ReviewsCount.self)

            self.reviewCounts.append(
                try nestedReviewCountContainer.decode(Int.self, forKey: .count)
            )
        }
    }
}

回答by Luca Angeletti

Many good answers have already been posted, but there is a simpler method not described yet IMO.

已经发布了许多好的答案,但是还有一种更简单的方法尚未在 IMO 中描述。

When the JSON field names are written using snake_case_notationyou can still use the camelCaseNotationin your Swift file.

当 JSON 字段名称使用 using 编写时,snake_case_notation您仍然可以camelCaseNotation在 Swift 文件中使用 。

You just need to set

你只需要设置

decoder.keyDecodingStrategy = .convertFromSnakeCase

After this ?? line Swift will automatically match all the snake_casefields from the JSON to the camelCasefields in the Swift model.

在这之后 ??line Swift 将自动snake_case将 JSON 中的所有camelCase字段与 Swift 模型中的字段进行匹配。

E.g.

例如

user_name` -> userName
reviews_count -> `reviewsCount
...

Here's the full code

这是完整的代码

1. Writing the Model

1. 编写模型

struct Response: Codable {

    let id: Int
    let user: User
    let reviewsCount: [ReviewCount]

    struct User: Codable {
        let userName: String

        struct RealInfo: Codable {
            let fullName: String
        }
    }

    struct ReviewCount: Codable {
        let count: Int
    }
}

2. Setting the Decoder

2. 设置解码器

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

3. Decoding

3.解码

do {
    let response = try? decoder.decode(Response.self, from: data)
    print(response)
} catch {
    debugPrint(error)
}

回答by simibac

  1. Copy the json file to https://app.quicktype.io
  2. Select Swift (if you use Swift 5, check the compatibility switch for Swift 5)
  3. Use the following code to decode the file
  4. Voila!
  1. 将json文件复制到https://app.quicktype.io
  2. 选择 Swift(如果您使用 Swift 5,请检查 Swift 5 的兼容性开关)
  3. 使用以下代码对文件进行解码
  4. 瞧!
let file = "data.json"

guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else{
    fatalError("Failed to locate \(file) in bundle.")
}

guard let data = try? Data(contentsOf: url) else{
    fatalError("Failed to locate \(file) in bundle.")
}

let yourObject = try? JSONDecoder().decode(YourModel.self, from: data)

回答by decybel

Also you can use library KeyedCodableI prepared. It will require less code. Let me know what you think about it.

你也可以使用我准备的库KeyedCodable。它将需要更少的代码。让我知道你对此的看法。

struct ServerResponse: Decodable, Keyedable {
  var id: String!
  var username: String!
  var fullName: String!
  var reviewCount: Int!

  private struct ReviewsCount: Codable {
    var count: Int
  }

  mutating func map(map: KeyMap) throws {
    var id: Int!
    try id <<- map["id"]
    self.id = String(id)

    try username <<- map["user.user_name"]
    try fullName <<- map["user.real_info.full_name"]

    var reviewCount: [ReviewsCount]!
    try reviewCount <<- map["reviews_count"]
    self.reviewCount = reviewCount[0].count
  }

  init(from decoder: Decoder) throws {
    try KeyedDecoder(with: decoder).decode(to: &self)
  }
}