Bagaimana cara memecahkan kode struct JSON bersarang dengan protokol Swift Decodable?

90

Ini JSON saya

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

Ini adalah struktur yang saya inginkan untuk disimpan (tidak lengkap)

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

Saya telah melihat Dokumentasi Apple tentang decoding struct bersarang, tetapi saya masih tidak mengerti bagaimana melakukan berbagai level JSON dengan benar. Bantuan apa pun akan sangat dihargai.

FlowUI. SimpleUITesting.com
sumber

Jawaban:

110

Pendekatan lain adalah membuat model perantara yang sangat cocok dengan JSON (dengan bantuan alat seperti quicktype.io ), biarkan Swift menghasilkan metode untuk mendekodekannya, lalu mengambil bagian yang Anda inginkan dalam model data akhir Anda:

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

Ini juga memungkinkan Anda untuk dengan mudah melakukan iterasi reviews_count, jika itu berisi lebih dari 1 nilai di masa mendatang.

Kode Berbeda
sumber
Baik. pendekatan ini terlihat sangat bersih. Untuk kasus saya, saya pikir saya akan menggunakannya
FlowUI. SimpleUITesting.com
Ya saya pasti terlalu memikirkan ini - @JTAppleCalendarforiOSSwift Anda harus menerimanya, karena ini adalah solusi yang lebih baik.
Hamish
@Hamish ok. saya menukarnya, tetapi jawaban Anda sangat rinci. Saya belajar banyak dari hal itu.
FlowUI. SimpleUITesting.com
Saya penasaran untuk mengetahui bagaimana seseorang dapat menerapkan Encodableuntuk ServerResponsestruktur mengikuti pendekatan yang sama. Apakah itu mungkin?
nayem
1
@nayem masalahnya ServerResponsememiliki lebih sedikit data daripada RawServerResponse. Anda dapat merekam RawServerResponseinstance, mengupdatenya dengan properti dari ServerResponse, lalu menghasilkan JSON darinya . Anda bisa mendapatkan bantuan yang lebih baik dengan memposting pertanyaan baru tentang masalah khusus yang Anda hadapi.
Kode Berbeda
95

Untuk memecahkan masalah Anda, Anda dapat membagi RawServerResponseimplementasi Anda menjadi beberapa bagian logika (menggunakan Swift 5).


# 1. Menerapkan properti dan kunci pengkodean yang diperlukan

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. Tetapkan strategi decoding untuk idproperti

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. Tetapkan strategi decoding untuk userNameproperti

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. Tetapkan strategi decoding untuk fullNameproperti

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. Tetapkan strategi decoding untuk reviewCountproperti

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
    }

}

Implementasi lengkap

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
    }

}

Pemakaian

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
*/
Imanou Petit
sumber
13
Jawaban yang sangat berdedikasi.
Hexfire
3
Bukan structAnda yang digunakan enumdengan kunci. yang jauh lebih elegan 👍
Jack
1
Terima kasih yang sebesar-besarnya karena telah meluangkan waktu untuk mendokumentasikannya dengan sangat baik. Setelah menjelajahi begitu banyak dokumentasi tentang Decodable dan parsing JSON, jawaban Anda benar-benar menjawab banyak pertanyaan yang saya miliki.
Marcy
30

Daripada memiliki satu CodingKeysenumerasi besar dengan semua kunci yang Anda perlukan untuk mendekode JSON, saya sarankan untuk memisahkan kunci untuk setiap objek JSON bersarang Anda, menggunakan enumerasi bersarang untuk mempertahankan hierarki:

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

Ini akan memudahkan untuk melacak kunci di setiap level di JSON Anda.

Sekarang, ingatlah bahwa:

  • Sebuah kontainer mengetik digunakan untuk memecahkan kode objek JSON, dan diterjemahkan dengan CodingKeyjenis sesuai (seperti yang kita telah didefinisikan di atas).

  • Sebuah wadah unkeyed digunakan untuk memecahkan kode array JSON, dan diterjemahkan secara berurutan (yaitu setiap kali Anda memanggil decode atau metode kontainer bersarang di atasnya, maju ke elemen berikutnya dalam array). Lihat bagian kedua dari jawaban untuk bagaimana Anda bisa mengulang satu.

Setelah mendapatkan kunci tingkat atas Anda penampung dari dekoder dengan container(keyedBy:)(karena Anda memiliki objek JSON di tingkat atas), Anda dapat berulang kali menggunakan metode:

Sebagai contoh:

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

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

Iterasi melalui wadah tanpa kunci

Mempertimbangkan kasus di mana Anda ingin reviewCountmenjadi [Int], di mana setiap elemen mewakili nilai untuk "count"kunci di JSON bertingkat:

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

Anda harus melakukan iterasi melalui penampung tanpa kunci bersarang, mendapatkan penampung kunci bersarang di setiap iterasi, dan mendekode nilai untuk "count"kunci tersebut. Anda dapat menggunakan countproperti dari kontainer tanpa kunci untuk mengalokasikan lebih dulu array yang dihasilkan, dan kemudian isAtEndproperti untuk melakukan iterasi melaluinya.

Sebagai contoh:

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)
            )
        }
    }
}
Hamish
sumber
satu hal yang perlu diperjelas: apa yang Anda maksud 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?
FlowUI. SimpleUITesting.com
@JTAppleCalendarforiOSSwift Maksud saya daripada memiliki satu CodingKeysenum besar dengan semua kunci yang Anda perlukan untuk memecahkan kode objek JSON Anda, Anda harus membaginya menjadi beberapa enum untuk setiap objek JSON - misalnya, dalam kode di atas yang kita miliki CodingKeys.Userdengan kunci untuk memecahkan kode objek JSON pengguna ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), jadi hanya kunci untuk "user_name"& "real_info".
Hamish
Terima kasih. Respon yang sangat jelas. Saya masih melihat-lihat untuk memahaminya sepenuhnya. Tapi itu berhasil.
FlowUI. SimpleUITesting.com
Saya punya satu pertanyaan tentang reviews_countyang merupakan array kamus. Saat ini, kodenya berfungsi seperti yang diharapkan. ReviewsCount saya hanya memiliki satu nilai dalam array. Tetapi bagaimana jika saya benar-benar menginginkan array review_count, maka saya hanya perlu mendeklarasikan var reviewCount: Intsebagai array, kan? -> var reviewCount: [Int]. Dan kemudian saya juga perlu mengedit ReviewsCountenum, kan?
FlowUI. SimpleUITesting.com
1
@JTAppleCalendarforiOSSwift Itu sebenarnya akan sedikit lebih rumit, karena yang Anda gambarkan bukan hanya larik Int, tetapi larik objek JSON yang masing-masing memiliki Intnilai untuk kunci tertentu - jadi yang perlu Anda lakukan adalah mengulanginya wadah yang tidak dikunci dan dapatkan semua wadah bertingkat yang dikunci, mendekode Intuntuk masing-masing wadah (dan kemudian menambahkannya ke larik Anda), misalnya gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish
4

Banyak jawaban bagus telah diposting, tetapi ada metode yang lebih sederhana yang belum dijelaskan IMO.

Saat nama kolom JSON ditulis menggunakan, snake_case_notationAnda masih dapat menggunakan camelCaseNotationdi file Swift Anda.

Anda hanya perlu mengatur

decoder.keyDecodingStrategy = .convertFromSnakeCase

Setelah ☝️ baris ini, Swift secara otomatis akan mencocokkan semua snake_casebidang dari JSON ke camelCasebidang dalam model Swift.

Misalnya

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

Berikut kode lengkapnya

1. Menulis 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. Mengatur Decoder

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

3. Decoding

do {
    let response = try? decoder.decode(Response.self, from: data)
    print(response)
} catch {
    debugPrint(error)
}
Luca Angeletti
sumber
2
Ini tidak menjawab pertanyaan awal bagaimana menangani berbagai tingkat bersarang.
Theo
2
  1. Salin file json ke https://app.quicktype.io
  2. Pilih Swift (jika Anda menggunakan Swift 5, periksa sakelar kompatibilitas untuk Swift 5)
  3. Gunakan kode berikut untuk memecahkan kode file
  4. Voila!
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)
simibac
sumber
1
Bekerja untuk saya, terima kasih. Situs itu emas. Untuk pemirsa, jika mendekode variabel string json jsonStr, Anda dapat menggunakan ini sebagai pengganti dua guard lets di atas: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }kemudian konversikan jsonStrDatake struct Anda seperti yang dijelaskan di atas pada let yourObjectbaris
Tanyakan P
Ini adalah alat yang luar biasa!
PostCodeism
0

Anda juga dapat menggunakan perpustakaan KeyedCodable yang saya siapkan. Ini akan membutuhkan lebih sedikit kode. Beri tahu saya pendapat Anda tentang itu.

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