Soru Swift Decodable protokolü ile iç içe JSON yapısını nasıl çözebilirim?


İşte benim JSON

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

Burada kaydetmek istediğim yapı (eksik)

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

Baktım Apple'ın Belgeleri iç içe geçmiş yapıları çözme üzerine, ancak JSON'un farklı düzeylerini nasıl düzgün bir şekilde yapılacağını hala anlamıyorum. Herhangi bir yardım çok takdir edilecektir.


44
2018-06-14 15:47


Menşei




Cevaplar:


Diğer bir yaklaşım ise JSON ile yakından eşleşen bir ara model oluşturmaktır, Swift'in kodu çözme yöntemlerini oluşturmasına izin verin ve son veri modelinizde istediğiniz parçaları seçin:

// snake_case to match the JSON
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
    }
}

Bu aynı zamanda kolayca yineleme yapmanızı sağlar reviews_countGelecekte 1'den fazla değer içermelidir.


46
2018-06-14 23:23



Tamam. Bu yaklaşım çok temiz görünüyor. Benim durumum için kullanacağımı düşünüyorum - iOS Calendar View OnMyProfile
Evet, kesinlikle bunu düşündüm - daha iyi bir çözüm olduğu için @JTAppleCalendarforiOSSwift kabul etmelisiniz. - Hamish
@Hamish tamam. Değiştirdim, ama cevabınız son derece detaylı. Ben ondan çok şey öğrendim. - iOS Calendar View OnMyProfile
Çok temiz bir çözüm, teşekkürler! - appsunited
Birinin nasıl uygulayacağını merak ediyorum Encodable için ServerResponse Aynı yaklaşımı takip eden yapı. Bu mümkün mü? - nayem


Probleminizi çözmek için RawServerResponse çeşitli mantık parçalarına uygulanması.


# 1. Özellikleri ve gerekli kodlama anahtarlarını uygulayı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

}

# 2. İçin kod çözme stratejisini ayarlayın id özellik

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.. İçin kod çözme stratejisini ayarlayın userName özellik

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.. İçin kod çözme stratejisini ayarlayın fullName özellik

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.. İçin kod çözme stratejisini ayarlayın reviewCount özellik

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
    }

}

Tamamlanmış uygulama

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
    }

}

kullanım

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

42
2017-07-11 20:52



Çok özel cevap. - Hexfire
Yerine struct kullandın enum tuşları ile. çok daha şık olan - Jack
Bu kadar iyi belgelemek için zaman ayırdığınız için çok teşekkür ederim. Decodable ve JSON'u ayrıştırmak için çok fazla belge inceledikten sonra, cevabınız gerçekten sahip olduğum birçok soruyu çözdü. - Marcy
Bu benim için kabul edilen cevap olmalı. - A_C


Büyük bir sahip olmak yerine CodingKeys ile numaralama herşey JSON kodunu çözmek için ihtiyaç duyacağınız tuşlar için her Hiyerarşiyi korumak için iç içe geçmiş numaralar kullanarak iç içe geçmiş JSON nesnelerinizin:

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

Bu, JSON’unuzdaki her bir tuştaki anahtarları takip etmenizi kolaylaştıracaktır.

Şimdi şunu aklınızda bulundurun:

  • bir anahtarlı konteyner bir JSON nesnesinin kodunu çözmek için kullanılır ve bir CodingKey Uygun tip (yukarıda tanımladığımız gibi).

  • bir anahtarsız kap JSON dizisini çözmek için kullanılır ve kodu çözülür sırayla (yani, her bir decode veya nested container yöntemi çağırdığınızda, dizideki bir sonraki öğeye ilerler). Birinden nasıl yineleyebileceğinize ilişkin cevabın ikinci bölümüne bakın.

Üst düzeyinizi aldıktan sonra anahtarlı ile kod çözücü container(keyedBy:) (üst düzeydeki bir JSON nesnesine sahip olduğunuz gibi), yöntemleri tekrar tekrar kullanabilirsiniz:

Örneğin:

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

Örnek kod çözme:

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)

Unkeyed bir kapsayıcıda yineleme

İstediğiniz durumu göz önünde bulundurarak reviewCount olmak [Int]her bir elemanın "count" iç içe JSON'da anahtar:

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

İç içe geçmiş, unkeyed kapsayıcısı boyunca yineleme yapmanız, her bir yinelemede yuvalanmış anahtarlı kapsayıcıyı almanız ve değerin "count" tuşuna basın. Kullanabilirsiniz count Sonuç dizisini önceden ayırmak için anahtarsız kapsayıcının özelliği ve sonra isAtEnd özelliği ile yineleme.

Örneğin:

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

17
2018-06-14 16:12



netleştirmek için bir şey: ne demek istediniz 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 ? - iOS Calendar View OnMyProfile
@JTAppleCalendarforiOSSwift Ben bir büyük sahip olmaktan ziyade demek istiyorum CodingKeys enum herşey JSON nesnesinin kodunu çözmeniz gereken anahtarlar, bunları her JSON nesnesi için birden çok numaraya bölmeniz gerekir - örneğin, yukarıdaki kodda CodingKeys.User kullanıcı JSON nesnesini çözme anahtarları ile{ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), sadece anahtarları "user_name" & "real_info". - Hamish
Teşekkürler. Çok açık cevap. Tamamen anlamak için hala bakıyorum. Ama işe yarıyor. - iOS Calendar View OnMyProfile
Hakkında bir sorum vardı reviews_count sözlükten oluşan bir dizidir. Şu anda, kod beklendiği gibi çalışır. İncelemelerim, yalnızca dizideki tek bir değere sahip. Ama ya gerçekten bir dizi review_count istediysem, o zaman basitçe beyan etmek zorundayım. var reviewCount: Int bir dizi olarak doğru mu? -> var reviewCount: [Int]. Ve sonra da düzenlemek zorundayım ReviewsCount enum doğru mu? - iOS Calendar View OnMyProfile
@JTAppleCalendarforiOSSwift Bu aslında biraz daha karmaşık olurdu, çünkü tanımladığınız şey sadece bir dizi değil Intancak her birinin sahip olduğu JSON nesneleri dizisi Int Belirli bir anahtar için değer - bu yüzden yapmanız gereken şey, şifrelenmemiş kapsayıcıda yineleme yapmak ve tüm iç içe geçmiş anahtar kapsayıcıları çözmektir. Int Her biri için (ve sonra bunları dizinize ekleme) gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41 - Hamish


Bu adamlar zaten sorumu cevapladı, ama bu bağlantıyı buraya gönderdiğimi düşündüm. https://app.quicktype.io/#l=swift

JSON yanıtınızı sol taraftaki bölmede yayınlamanız yeterlidir ve Modellerinizin sağda oluşturulduğunu izleyin. Bu sadece senin başladığında yardımcı olabilir.


9
2018-03-01 01:07



Harika kaynak - Sadece bunu öğrenmeye başlıyorum. - Chris


Ayrıca kütüphaneyi de kullanabilirsiniz KeyedCodable Hazırladım. Daha az kod gerektirecektir. Bunun hakkında ne düşündüğünüzü bana bildirin.

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

0
2018-05-27 20:13