Swift Çözülebilir protokolü ile iç içe geçmiş JSON yapısının kodu nasıl çözülür?


94

İşte JSON'um

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

İşte 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?
    }
}

Apple'ın iç içe geçmiş yapıların kodunu çözme konusundaki Belgelerine baktım , ancak hala JSON'un farklı seviyelerinin nasıl doğru bir şekilde yapılacağını anlamıyorum. Herhangi bir yardım çok takdir edilecektir.

Yanıtlar:


113

Diğer bir yaklaşım, JSON ile yakından eşleşen bir ara model oluşturmaktır ( quicktype.io gibi bir aracın yardımıyla ), Swift'in onu çözmek için yöntemler oluşturmasına izin verin ve ardından son veri modelinizde istediğiniz parçaları seçin:

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

Bu ayrıca reviews_count, gelecekte 1'den fazla değer içermesi durumunda kolayca yineleme yapmanıza da olanak tanır .


Tamam. bu yaklaşım çok temiz görünüyor. Benim durumum için, onu kullanacağımı düşünüyorum
Sadece bir kodlayıcı

Evet, kesinlikle bunu düşündüm - @JTAppleCalendarforiOSSwift, daha iyi bir çözüm olduğu için kabul etmelisin.
Hamish

@Hamish ok. değiştirdim ama cevabınız son derece ayrıntılıydı. Ben ondan çok şey öğrendim.
Sadece kodlayıcı

Aynı yaklaşımı izleyen yapı Encodableiçin nasıl uygulanabileceğini merak ediyorum ServerResponse. Hatta mümkün mü?
nayem

1
@nayem sorun ServerResponsedaha az veriye sahip RawServerResponse. Örneği yakalayabilir, RawServerResponseiçindeki özelliklerle güncelleyebilir ServerResponse, ardından bundan JSON oluşturabilirsiniz. Karşılaştığınız belirli bir sorunla ilgili yeni bir soru göndererek daha iyi yardım alabilirsiniz.
Kod Farklı

100

Sorununuzu çözmek için, RawServerResponseuygulamanızı birkaç mantık parçasına ayırabilirsiniz (Swift 5'i kullanarak).


# 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. idMülk için kod çözme stratejisini ayarlayın

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. userNameMülk için kod çözme stratejisini ayarlayın

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. fullNameMülk için kod çözme stratejisini ayarlayın

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. reviewCountMülk için kod çözme stratejisini ayarlayın

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
    }

}

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

14
Çok özel cevap.
Hexfire

3
structSenin yerine enumanahtarlarla kullan. bu çok daha zarif 👍
Jack

1
Bunu bu kadar iyi belgelemeye zaman ayırdığınız için çok teşekkür ederim. Çözülebilirlikle ilgili bu kadar çok dokümantasyonu inceledikten ve JSON'u ayrıştırdıktan sonra, cevabınız sahip olduğum birçok soruyu gerçekten açıklığa kavuşturdu.
Marcy

31

JSON'un kodunu çözmek için ihtiyaç duyacağınız tüm anahtarlara sahip büyük bir CodingKeysnumaralandırmaya sahip olmak yerine , hiyerarşiyi korumak için iç içe numaralandırmaları kullanarak iç içe yerleştirilmiş JSON nesnelerinizin her biri için anahtarları bölmenizi tavsiye ederim :

// 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'nizdeki her seviyedeki anahtarları takip etmeyi kolaylaştıracaktır.

Şimdi şunu unutmayın:

  • Bir anahtarlı kap bir JSON nesnesi kodunu çözmek için kullanılır ve bir ile kodu çözülür CodingKeyuygun tip (olanlar gibi, yukarıda tanımlanan ettik).

  • Bir JSON dizisinin kodunu çözmek için anahtarlanmamış bir kap kullanılır ve sıralı olarak kodu çözülür (yani, üzerinde bir kod çözme veya iç içe geçmiş kap yöntemi her çağırdığınızda, dizideki bir sonraki öğeye ilerler). Birini nasıl tekrarlayabileceğinizi öğrenmek için cevabın ikinci bölümüne bakın.

En üst düzey anahtarlı kapsayıcınızı kod çözücüden aldıktan sonra (üst düzeyde container(keyedBy:)bir JSON nesneniz olduğu için), 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)

Anahtarsız bir kapsayıcıda yineleme

reviewCountAn olmak istediğiniz durumu göz önünde bulundurarak, [Int]her öğenin "count"iç içe yerleştirilmiş JSON'daki anahtarın değerini temsil ettiği durumlarda :

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

İç içe geçmiş anahtarlanmamış kapsayıcıda yinelemeniz, her yinelemede yuvalanmış anahtarlı kapsayıcıyı almanız ve "count"anahtarın değerini çözmeniz gerekir . Kullanabilirsiniz countsonra elde edilen bir dizi ön-ayırma ve amacıyla anahtarlanmamış kabın özelliği isAtEndiçinden yineleme için özelliği.

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

Açıklığa kavuşturulacak bir şey var: 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?
Just a coder

@JTAppleCalendarforiOSSwift Demek istediğim, JSON nesnenizin kodunu çözmeniz gereken tüm anahtarlara sahip büyük bir CodingKeysnumaralandırmaya sahip olmak yerine , bunları her JSON nesnesi için birden çok numaralandırmaya ayırmanız gerekir - örneğin, yukarıdaki kodda anahtarlarla birlikte kullanıcı JSON nesnesinin ( ) kodunu çözmek için , yalnızca & . CodingKeys.User{ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }"user_name""real_info"
Hamish

Teşekkürler. Çok net yanıt. Hâlâ tam olarak anlamak için araştırıyorum. Ama işe yarıyor.
Just a coder

reviews_countHangisinin bir sözlük dizisi olduğu hakkında bir sorum vardı . Şu anda kod beklendiği gibi çalışıyor. My reviewsCount dizide yalnızca bir değere sahiptir. Ama ya gerçekten bir review_count dizisi istersem, o zaman basitçe var reviewCount: Intbir dizi olarak bildirmem gerekir, değil mi? -> var reviewCount: [Int]. Ve sonra sıralamayı da düzenlemem gerekir, ReviewsCountdeğil mi?
Just a coder

1
@JTAppleCalendarforiOSSwift Bu aslında biraz daha karmaşık olurdu, çünkü tanımladığınız şey sadece bir dizi değil Int, her biri Intbelirli bir anahtar için bir değere sahip olan bir dizi JSON nesnesi - yani yapmanız gereken şey yinelemek anahtarsız kapsayıcı ve tüm iç içe geçmiş anahtarlı kapsayıcıları alın, Inther biri için bir kod çözme (ve ardından bunları dizinize ekleyerek), örneğin gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

Pek çok iyi cevap zaten yayınlandı, ancak IMO'nun henüz açıklanmadığı daha basit bir yöntem var.

JSON alan adları kullanılarak yazıldığında snake_case_notation, yine de camelCaseNotationSwift dosyanızda kullanabilirsiniz.

Sadece ayarlaman gerekiyor

decoder.keyDecodingStrategy = .convertFromSnakeCase

Bu ☝️ satırından sonra Swift, JSON'dan Swift modelindeki snake_casealanlara kadar tüm alanları otomatik olarak eşleştirecektir camelCase.

Örneğin

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

İşte tam kod

1. Modeli Yazmak

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. Dekoderi Ayarlama

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

3. Kod çözme

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

2
Bu, farklı yuvalama düzeyleriyle nasıl başa çıkılacağı sorusuna yanıt vermez.
Theo

3
  1. Json dosyasını https: //app.quicktype.io'ya kopyalayın
  2. Swift'i seçin (Swift 5 kullanıyorsanız, Swift 5 için uyumluluk anahtarını kontrol edin)
  3. Dosyanın kodunu çözmek için aşağıdaki kodu kullanın
  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)

1
Benim için çalıştı, teşekkürler. Bu site altın. İzleyiciler için, bir json dize değişkeninin kodunu çözüyorsanız jsonStr, guard letyukarıdaki iki s yerine bunu kullanabilirsiniz : guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }sonra jsonStrData, let yourObjectsatırda yukarıda açıklandığı gibi yapınıza dönüştürün
P

Bu harika bir araçtır!
PostCodeism

0

Ayrıca hazırladığım KeyedCodable kütüphanesini de kullanabilirsiniz . 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)
  }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.