Hoe een geneste JSON-struct decoderen met het Swift Decodable-protocol?

Hier is mijn JSON

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

Hier is de structuur waarin ik het wil bewaren (onvolledig)

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

Ik heb Apple’s documentatieover het decoderen van geneste structuren bekeken, maar ik heb nog steeds begrijp niet hoe de verschillende niveaus van de JSON correct moeten worden uitgevoerd. Alle hulp wordt zeer op prijs gesteld.


Antwoord 1, autoriteit 100%

Een andere benadering is om een ​​tussenmodel te maken dat nauw aansluit bij de JSON (met behulp van een tool als quicktype.io), laat Swift de methoden genereren om het te decoderen, en kies vervolgens de stukjes die je wilt in je uiteindelijke gegevensmodel:

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

Hierdoor kunt u ook gemakkelijk reviews_countdoorlopen, mocht het in de toekomst meer dan 1 waarde bevatten.


Antwoord 2, autoriteit 95%

Om uw probleem op te lossen, kunt u uw RawServerResponse-implementatie opsplitsen in verschillende logische delen (met Swift 5).


#1. Implementeer de eigenschappen en vereiste coderingssleutels

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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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. Stel de decoderingsstrategie in voor de eigenschap 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
    }
}

Voltooide implementatie

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

Gebruik

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

Antwoord 3, autoriteit 26%

In plaats van één grote CodingKeysopsomming te hebben met allesleutels die je nodig hebt voor het decoderen van de JSON, zou ik adviseren de sleutels op te splitsen voor elkvan uw geneste JSON-objecten, met behulp van geneste opsommingen om de hiërarchie te behouden:

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

Dit maakt het gemakkelijker om de sleutels op elk niveau in uw JSON bij te houden.

Nu, rekening houdend met het volgende:

  • Een gecodeerde containerwordt gebruikt om een ​​JSON-object te decoderen en is gedecodeerd met een CodingKeyconform type (zoals degene die we heb hierboven gedefinieerd).

  • Een unkeyed containerwordt gebruikt om een ​​JSON-array te decoderen en is gedecodeerd opeenvolgend(dwz elke keer dat u een decodeer- of geneste containermethode erop aanroept, gaat deze door naar het volgende element in de array). Zie het tweede deel van het antwoord voor hoe je er een kunt herhalen.

Nadat u uw versleuteldecontainer op het hoogste niveau van de decoder hebt gehaald met container(keyedBy:)(omdat je een JSON-object op het hoogste niveau hebt), kun je herhaaldelijk de methoden gebruiken:

Bijvoorbeeld:

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

Voorbeeld decodering:

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)

Itereren door een niet-gecodeerde container

Gezien het geval waarin u wilt dat reviewCounteen [Int]is, waarbij elk element de waarde vertegenwoordigt voor de sleutel "count"in de geneste JSON:

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

U moet de geneste niet-gecodeerde container doorlopen, de geneste versleutelde container bij elke herhaling ophalen en de waarde voor de sleutel "count"decoderen. U kunt de eigenschap countvan de unkeyed container om de resulterende array vooraf toe te wijzen, en vervolgens de isAtEndeigenschap om er doorheen te bladeren.

Bijvoorbeeld:

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

Antwoord 4, autoriteit 5%

  1. Kopieer het json-bestand naar https://app.quicktype.io
  2. Selecteer Swift (als je Swift 5 gebruikt, controleer dan de compatibiliteitsschakelaar voor Swift 5)
  3. Gebruik de volgende code om het bestand te decoderen
  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)

Antwoord 5, autoriteit 5%

Er zijn al veel goede antwoorden gepost, maar er is een eenvoudigere methode die IMO nog niet is beschreven.

Als de JSON-veldnamen zijn geschreven met snake_case_notation, kun je nog steeds de camelCaseNotationin je Swift-bestand gebruiken.

Je hoeft alleen maar in te stellen

decoder.keyDecodingStrategy = .convertFromSnakeCase

Na deze ☝️-regel zal Swift automatisch alle snake_case-velden van de JSON matchen met de camelCase-velden in het Swift-model.

Bijvoorbeeld

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

Hier is de volledige code

1. Het model schrijven

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. De decoder instellen

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

3. Decodering

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

Antwoord 6

U kunt ook de bibliotheek KeyedCodablegebruiken die ik heb voorbereid. Er is minder code nodig. Laat me weten wat je ervan vindt.

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

Other episodes