Làm cách nào để giải mã một cấu trúc JSON lồng nhau bằng giao thức Swift Decodable?


90

Đây là JSON của tôi

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

Đây là cấu trúc tôi muốn nó được lưu vào (chưa hoàn thành)

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?
    }
}

Tôi đã xem Tài liệu của Apple về giải mã cấu trúc lồng nhau, nhưng tôi vẫn không hiểu cách thực hiện các cấp khác nhau của JSON đúng cách. Bất kỳ sự giúp đỡ sẽ được nhiều đánh giá cao.

Câu trả lời:


109

Một cách tiếp cận khác là tạo một mô hình trung gian khớp chặt chẽ với JSON (với sự trợ giúp của một công cụ như quicktype.io ), để Swift tạo các phương thức để giải mã nó, rồi chọn các phần bạn muốn trong mô hình dữ liệu cuối cùng của mình:

// 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
    }
}

Điều này cũng cho phép bạn dễ dàng lặp lại reviews_count, nếu nó chứa nhiều hơn 1 giá trị trong tương lai.


Đồng ý. cách tiếp cận này trông rất sạch sẽ. Đối với trường hợp của tôi, tôi nghĩ tôi sẽ sử dụng nó
FlowUI. SimpleUITesting.com

Vâng, tôi chắc chắn đã suy nghĩ kỹ điều này - @JTAppleCalendarforiOSSwift, bạn nên chấp nhận nó, vì đó là một giải pháp tốt hơn.
Hamish

@ Hamish ok. tôi đã chuyển nó, nhưng câu trả lời của bạn rất chi tiết. Tôi học được rất nhiều từ nó.
FlowUI. SimpleUITesting.com

Tôi tò mò muốn biết làm thế nào người ta có thể triển khai Encodablecho ServerResponsecấu trúc theo cùng một cách tiếp cận. Nó thậm chí có thể?
nayem

1
@nayem vấn đề là ServerResponsecó ít dữ liệu hơn RawServerResponse. Bạn có thể nắm bắt RawServerResponsecá thể, cập nhật nó với các thuộc tính từ ServerResponseđó, sau đó tạo JSON từ đó. Bạn có thể nhận được trợ giúp tốt hơn bằng cách đăng một câu hỏi mới với vấn đề cụ thể mà bạn đang gặp phải.
Mã khác

95

Để giải quyết vấn đề của mình, bạn có thể chia RawServerResponsequá trình triển khai của mình thành nhiều phần logic (sử dụng Swift 5).


# 1. Triển khai các thuộc tính và khóa mã hóa bắt buộc

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. Đặt chiến lược giải mã cho thuộc idtính

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. Đặt chiến lược giải mã cho thuộc userNametính

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. Đặt chiến lược giải mã cho thuộc fullNametính

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. Đặt chiến lược giải mã cho thuộc reviewCounttính

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
    }

}

Hoàn thành thực hiện

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
    }

}

Sử dụng

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
*/

13
Câu trả lời rất tận tình.
Hexfire

3
Thay vì structbạn sử dụng enumvới chìa khóa. thanh lịch hơn nhiều 👍
Jack

1
Rất cảm ơn bạn đã dành thời gian để ghi lại điều này rất tốt. Sau khi tìm kiếm rất nhiều tài liệu về JSON có thể giải mã và phân tích cú pháp, câu trả lời của bạn thực sự đã giải đáp được nhiều thắc mắc của tôi.
Marcy

30

Thay vì có một CodingKeysbảng liệt kê lớn với tất cả các khóa bạn sẽ cần để giải mã JSON, tôi khuyên bạn nên chia nhỏ các khóa cho từng đối tượng JSON lồng nhau của bạn, sử dụng các kiểu liệt kê lồng nhau để duy trì hệ thống phân cấp:

// 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
    }
}

Điều này sẽ giúp bạn dễ dàng theo dõi các khóa ở mỗi cấp độ trong JSON của bạn.

Bây giờ, hãy ghi nhớ rằng:

  • Một vùng chứa có khóa được sử dụng để giải mã một đối tượng JSON và được giải mã bằng một CodingKeykiểu phù hợp (chẳng hạn như những kiểu chúng ta đã xác định ở trên).

  • Một vùng chứa không khóa được sử dụng để giải mã một mảng JSON và được giải mã tuần tự (tức là mỗi khi bạn gọi một phương thức giải mã hoặc phương thức vùng chứa lồng nhau trên đó, nó sẽ chuyển sang phần tử tiếp theo trong mảng). Xem phần thứ hai của câu trả lời để biết cách bạn có thể lặp lại qua một.

Sau khi nhận vùng chứa có khóa cấp cao nhất của bạn từ bộ giải mã với container(keyedBy:)(vì bạn có đối tượng JSON ở cấp cao nhất), bạn có thể sử dụng nhiều lần các phương pháp:

Ví dụ:

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)
    }
}

Giải mã ví dụ:

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)

Lặp lại thông qua một vùng chứa không khóa

Xem xét trường hợp bạn muốn reviewCounttrở thành một [Int], trong đó mỗi phần tử đại diện cho giá trị cho "count"khóa trong JSON lồng nhau:

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

Bạn sẽ cần phải lặp lại vùng chứa không khóa lồng nhau, nhận vùng chứa có khóa lồng nhau ở mỗi lần lặp và giải mã giá trị cho "count"khóa. Bạn có thể sử dụng thuộc counttính của vùng chứa không có khóa để cấp phát trước mảng kết quả và sau đó là thuộc isAtEndtính để lặp qua nó.

Ví dụ:

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)
            )
        }
    }
}

một điều cần làm rõ: ý bạn là I would advise splitting the keys for each of your nested JSON objects up into multiple nested enumerations, thereby making it easier to keep track of the keys at each level in your JSONgì?
FlowUI. SimpleUITesting.com

@JTAppleCalendarforiOSSwift Ý tôi là thay vì có một CodingKeysenum lớn với tất cả các khóa bạn sẽ cần để giải mã đối tượng JSON của mình, bạn nên chia chúng thành nhiều enum cho mỗi đối tượng JSON - ví dụ: trong đoạn mã trên, chúng tôi có CodingKeys.Usercác khóa để giải mã đối tượng JSON của người dùng ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), vì vậy chỉ cần các khóa cho "user_name"& "real_info".
Hamish

Cảm ơn. Phản ứng rất rõ ràng. Tôi vẫn đang nhìn qua nó để hiểu nó đầy đủ. Nhưng nó đã có tác dụng.
FlowUI. SimpleUITesting.com

Tôi có một câu hỏi về cái reviews_countđó là một mảng từ điển. Hiện tại, mã hoạt động như mong đợi. Bài đánh giá của tôi Số tiền chỉ có một giá trị trong mảng. Nhưng nếu tôi thực sự muốn một mảng review_count, thì tôi chỉ cần khai báo var reviewCount: Intdưới dạng một mảng phải không? -> var reviewCount: [Int]. Và sau đó tôi cũng cần phải chỉnh sửa ReviewsCountenum phải không?
FlowUI. SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift Điều đó thực sự sẽ phức tạp hơn một chút, vì những gì bạn đang mô tả không chỉ là một mảng Intmà là một mảng các đối tượng JSON mà mỗi đối tượng có một Intgiá trị cho một khóa nhất định - vì vậy những gì bạn cần làm là lặp lại container unkeyed và nhận được tất cả các container có khóa lồng nhau, giải mã một Intcho mỗi người (và sau đó gắn thêm những mảng của bạn), ví dụ như gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

Nhiều câu trả lời hay đã được đăng, nhưng có một phương pháp đơn giản hơn chưa được IMO mô tả.

Khi tên trường JSON được viết bằng cách sử dụng, snake_case_notationbạn vẫn có thể sử dụng camelCaseNotationtrong tệp Swift của mình.

Bạn chỉ cần đặt

decoder.keyDecodingStrategy = .convertFromSnakeCase

Sau dòng ☝️ này, Swift sẽ tự động khớp tất cả các snake_casetrường từ JSON với các camelCasetrường trong mô hình Swift.

Ví dụ

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

Đây là mã đầy đủ

1. Viết Mô hình

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. Đặt bộ giải mã

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

3. Giải mã

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

2
Điều này không giải quyết được câu hỏi ban đầu là làm thế nào để đối phó với các mức độ lồng ghép khác nhau.
Theo

2
  1. Sao chép tệp json vào https://app.quicktype.io
  2. Chọn Swift (nếu bạn sử dụng Swift 5, hãy kiểm tra công tắc tương thích cho Swift 5)
  3. Sử dụng mã sau để giải mã tệp
  4. Thì đấy!
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)

1
Làm việc cho tôi, cảm ơn bạn. Trang web đó là vàng. Đối với khán giả, nếu giải mã một biến chuỗi json jsonStr, bạn có thể sử dụng thay vì hai guard lets trên: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }sau đó chuyển đổi jsonStrDatađể struct của bạn như mô tả ở trên trên let yourObjectdòng
Hỏi P

Đây là một công cụ tuyệt vời!
PostCodeism

0

Ngoài ra bạn có thể sử dụng thư viện KeyedCodable mà tôi đã chuẩn bị. Nó sẽ yêu cầu ít mã hơn. Hãy cho tôi biết những gì bạn nghĩ về nó.

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)
  }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.