จะถอดรหัสโครงสร้าง JSON ที่ซ้อนกันด้วยโปรโตคอล Swift Decodable ได้อย่างไร


94

นี่คือ JSON ของฉัน

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

นี่คือโครงสร้างที่ฉันต้องการบันทึกไว้ (ไม่สมบูรณ์)

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

ฉันได้ดูเอกสารของ Appleเกี่ยวกับการถอดรหัสโครงสร้างที่ซ้อนกันแล้ว แต่ฉันยังไม่เข้าใจวิธีทำ JSON ระดับต่างๆอย่างถูกต้อง ความช่วยเหลือใด ๆ จะได้รับการชื่นชมมาก

คำตอบ:


113

อีกวิธีหนึ่งคือการสร้างโมเดลระดับกลางที่ตรงกับ 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
    }
}

นอกจากนี้ยังช่วยให้คุณวนซ้ำได้อย่างง่ายดายreviews_countหากมีมากกว่า 1 ค่าในอนาคต


ตกลง. แนวทางนี้ดูสะอาดมาก สำหรับกรณีของฉันฉันคิดว่าฉันจะใช้มัน
แค่ coder

ใช่ฉันคิดมากเกินไป - @JTAppleCalendarforiOSSwift คุณควรยอมรับเพราะมันเป็นทางออกที่ดีกว่า
Hamish

@ ฮามิชโอเค ฉันเปลี่ยนแล้ว แต่คำตอบของคุณมีรายละเอียดมาก ฉันได้เรียนรู้มากมายจากมัน
แค่คนเขียนโค้ด

ฉันอยากจะรู้ว่าวิธีการหนึ่งที่สามารถใช้EncodableสำหรับServerResponseโครงสร้างต่อไปนี้วิธีการเดียวกัน เป็นไปได้หรือไม่?
nayem

1
@nayem ปัญหาServerResponseมีข้อมูลน้อยกว่าRawServerResponse. คุณสามารถจับภาพRawServerResponseอินสแตนซ์อัปเดตด้วยคุณสมบัติจากServerResponseนั้นสร้าง JSON จากสิ่งนั้น คุณสามารถรับความช่วยเหลือได้ดีขึ้นโดยโพสต์คำถามใหม่พร้อมกับปัญหาเฉพาะที่คุณกำลังเผชิญ
Code Different

99

ในการแก้ปัญหาของคุณคุณสามารถแบ่งRawServerResponseการใช้งานออกเป็นส่วนตรรกะหลาย ๆ ส่วน (โดยใช้ Swift 5)


# 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. กำหนดกลยุทธ์การถอดรหัสสำหรับ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. กำหนดกลยุทธ์การถอดรหัสสำหรับ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. กำหนดกลยุทธ์การถอดรหัสสำหรับ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. กำหนดกลยุทธ์การถอดรหัสสำหรับ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
    }

}

ใช้งานได้อย่างสมบูรณ์

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
    }

}

การใช้งาน

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

14
คำตอบที่ทุ่มเทมาก
Hexfire

3
แทนคุณstructใช้enumกับกุญแจ สวยหรูกว่าเยอะ👍
Jack

1
ขอบคุณมากที่สละเวลาในการจัดทำเอกสารนี้เป็นอย่างดี หลังจากสแกนเอกสารมากมายเกี่ยวกับ Decodable และแยกวิเคราะห์ JSON คำตอบของคุณก็เคลียร์คำถามมากมายที่ฉันมี
Marcy

30

แทนที่จะมีการCodingKeysแจกแจงจำนวนมากพร้อมกับคีย์ทั้งหมดที่คุณต้องใช้ในการถอดรหัส JSON ฉันขอแนะนำให้แยกคีย์สำหรับแต่ละอ็อบเจ็กต์ 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
    }
}

วิธีนี้จะทำให้ง่ายต่อการติดตามคีย์ในแต่ละระดับใน JSON ของคุณ

ตอนนี้โปรดจำไว้ว่า:

  • ภาชนะคีย์จะใช้ในการถอดรหัสวัตถุ JSON และถอดรหัสกับCodingKeyประเภทสอดคล้อง (เช่นคนที่เราได้กำหนดไว้ด้านบน)

  • ภาชนะ unkeyedจะใช้ในการถอดรหัสอาร์เรย์ JSON และถอดรหัสตามลำดับ (เช่นทุกครั้งที่คุณโทรถอดรหัสหรือวิธีคอนเทนเนอร์ซ้อนกันที่มันก้าวหน้าไปยังองค์ประกอบถัดไปในอาร์เรย์) ดูส่วนที่สองของคำตอบสำหรับวิธีการวนซ้ำ

หลังจากรับคอนเทนเนอร์คีย์ระดับบนสุดจากตัวถอดรหัสด้วยcontainer(keyedBy:)(เนื่องจากคุณมีออบเจ็กต์ JSON ที่ระดับบนสุด) คุณสามารถใช้วิธีการซ้ำ ๆ :

  • nestedContainer(keyedBy:forKey:) เพื่อรับวัตถุที่ซ้อนกันจากวัตถุสำหรับคีย์ที่กำหนด
  • nestedUnkeyedContainer(forKey:) เพื่อรับอาร์เรย์ที่ซ้อนกันจากวัตถุสำหรับคีย์ที่กำหนด
  • nestedContainer(keyedBy:) เพื่อรับวัตถุที่ซ้อนกันถัดไปจากอาร์เรย์
  • nestedUnkeyedContainer() เพื่อรับอาร์เรย์ที่ซ้อนกันถัดไปจากอาร์เรย์

ตัวอย่างเช่น:

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

ตัวอย่างการถอดรหัส:

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)

ทำซ้ำผ่านคอนเทนเนอร์ที่ไม่ได้ใส่กุญแจ

พิจารณากรณีที่คุณต้องการreviewCountเป็น[Int]โดยที่แต่ละองค์ประกอบแทนค่าสำหรับ"count"คีย์ใน JSON ที่ซ้อนกัน:

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

คุณจะต้องวนซ้ำผ่านคอนเทนเนอร์ที่ไม่ได้ใส่คีย์ที่ซ้อนกันรับคอนเทนเนอร์คีย์ที่ซ้อนกันในการวนซ้ำแต่ละครั้งและถอดรหัสค่าของ"count"คีย์ คุณสามารถใช้countคุณสมบัติของคอนเทนเนอร์ที่ไม่ได้ใส่คีย์เพื่อจัดสรรอาร์เรย์ผลลัพธ์ล่วงหน้าจากนั้นisAtEndคุณสมบัติเพื่อวนซ้ำผ่านมัน

ตัวอย่างเช่น:

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

สิ่งหนึ่งที่ต้องชี้แจง: คุณหมายถึง 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 JSONอะไร?
แค่คนเขียนโค้ด

@JTAppleCalendarforiOSSwift ฉันหมายความว่าแทนที่จะมีCodingKeysenum ขนาดใหญ่พร้อมกับคีย์ทั้งหมดที่คุณต้องใช้ในการถอดรหัสออบเจ็กต์ JSON ของคุณคุณควรแบ่งออกเป็นหลาย enums สำหรับแต่ละออบเจ็กต์ JSON - ตัวอย่างเช่นในโค้ดด้านบนที่เรามีCodingKeys.Userกับคีย์ เพื่อถอดรหัสออบเจ็กต์ JSON ของผู้ใช้ ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }) ดังนั้นเพียงแค่คีย์สำหรับ"user_name"& "real_info".
Hamish

ขอบคุณ. ตอบสนองชัดเจนมาก ฉันยังคงมองผ่านมันเพื่อทำความเข้าใจอย่างถ่องแท้ แต่มันได้ผล
เพียงแค่ coder

ฉันมีคำถามหนึ่งข้อเกี่ยวกับreviews_countซึ่งเป็นอาร์เรย์ของพจนานุกรม ขณะนี้รหัสทำงานตามที่คาดไว้ reviewsCount ของฉันเคยมีเพียงค่าเดียวในอาร์เรย์ แต่ถ้าฉันต้องการอาร์เรย์ของ review_count จริงฉันก็ต้องประกาศvar reviewCount: Intว่าเป็นอาร์เรย์ใช่ไหม -> var reviewCount: [Int]. แล้วฉันต้องแก้ไขReviewsCountenum ด้วยใช่ไหม
แค่คนเขียนโค้ด

1
@JTAppleCalendarforiOSSwift นั่นจะซับซ้อนกว่าเล็กน้อยเนื่องจากสิ่งที่คุณอธิบายไม่ใช่แค่อาร์เรย์Intแต่เป็นอาร์เรย์ของออบเจ็กต์ JSON ที่แต่ละอันมีIntค่าสำหรับคีย์ที่กำหนดดังนั้นสิ่งที่คุณต้องทำคือทำซ้ำ คอนเทนเนอร์ที่ไม่ได้ใส่คีย์และรับคอนเทนเนอร์คีย์ที่ซ้อนกันทั้งหมดถอดรหัสIntสำหรับแต่ละคอนเทนเนอร์(จากนั้นต่อท้ายอาร์เรย์ของคุณ) เช่นgist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

มีการโพสต์คำตอบที่ดีมากมายแล้ว แต่ยังมีวิธีที่ง่ายกว่านี้ซึ่งยังไม่ได้อธิบาย IMO

เมื่อชื่อฟิลด์ JSON ถูกเขียนโดยใช้snake_case_notationคุณยังสามารถใช้camelCaseNotationในไฟล์ Swift ของคุณได้

คุณเพียงแค่ต้องตั้งค่า

decoder.keyDecodingStrategy = .convertFromSnakeCase

หลังจากนี้☝️บรรทัด Swift จะจับคู่snake_caseฟิลด์ทั้งหมดจาก JSON กับcamelCaseฟิลด์ในโมเดล Swift โดยอัตโนมัติ

เช่น

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

นี่คือรหัสฉบับเต็ม

1. การเขียน Model

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. การตั้งค่าตัวถอดรหัส

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

3. การถอดรหัส

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

2
สิ่งนี้ไม่ได้ตอบคำถามเดิมว่าจะจัดการกับการซ้อนในระดับต่างๆอย่างไร
ธีโอ

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

1
ทำงานให้ฉันขอบคุณ ไซต์นั้นเป็นสีทอง สำหรับผู้ชมหากถอดรหัสตัวแปรสตริง json jsonStrคุณสามารถใช้สิ่งนี้แทนสองตัวguard letด้านบนguard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }จากนั้นแปลงjsonStrDataเป็นโครงสร้างของคุณตามที่อธิบายไว้ข้างต้นในlet yourObjectบรรทัด
ถาม P

นี่คือเครื่องมือที่น่าทึ่ง!
PostCodeism

0

นอกจากนี้คุณสามารถใช้ไลบรารี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)
  }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.