Wie dekodiere ich eine verschachtelte JSON-Struktur mit dem Swift Decodable-Protokoll?


88

Hier ist mein JSON

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

Hier ist die Struktur, in der ich sie speichern möchte (unvollständig)

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

Ich habe mir Apples Dokumentation zum Dekodieren verschachtelter Strukturen angesehen, verstehe aber immer noch nicht, wie die verschiedenen Ebenen des JSON richtig ausgeführt werden. Jede Hilfe wird sehr geschätzt.

Antworten:


106

Ein anderer Ansatz besteht darin, ein Zwischenmodell zu erstellen, das eng mit dem JSON übereinstimmt (mithilfe eines Tools wie quicktype.io ), Swift die Methoden zum Dekodieren generieren zu lassen und dann die gewünschten Teile in Ihrem endgültigen Datenmodell auszuwählen:

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

Auf diese Weise können Sie auch problemlos durchlaufen reviews_count, falls es in Zukunft mehr als einen Wert enthält.


OK. Dieser Ansatz sieht sehr sauber aus. Für meinen Fall denke ich, dass ich es verwenden werde
FlowUI. SimpleUITesting.com

Ja, ich habe das definitiv überlegt - @JTAppleCalendarforiOSSwift, du solltest es akzeptieren, da es eine bessere Lösung ist.
Hamish

@ Hamish ok. Ich habe es gewechselt, aber Ihre Antwort war äußerst detailliert. Ich habe viel daraus gelernt.
FlowUI. SimpleUITesting.com

Ich bin gespannt, wie man das umsetzen kann Encodable die ServerResponseStruktur nach dem gleichen Ansatz . Ist es überhaupt möglich?
Nayem

1
@ayem das Problem ist, ServerResponsehat weniger Daten als RawServerResponse. Sie können die RawServerResponseInstanz erfassen , mit Eigenschaften von aktualisieren ServerResponseund dann den JSON daraus generieren. Sie können bessere Hilfe erhalten, indem Sie eine neue Frage mit dem spezifischen Problem stellen, mit dem Sie konfrontiert sind.
Code anders

94

Um Ihr Problem zu lösen, können Sie Ihre RawServerResponseImplementierung in mehrere logische Teile aufteilen (mit Swift 5).


# 1. Implementieren Sie die Eigenschaften und erforderlichen Codierungsschlüssel

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. Legen Sie die Dekodierungsstrategie für die idEigenschaft fest

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. Legen Sie die Dekodierungsstrategie für die userNameEigenschaft fest

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. Legen Sie die Dekodierungsstrategie für die fullNameEigenschaft fest

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. Legen Sie die Dekodierungsstrategie für die reviewCountEigenschaft fest

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
    }

}

Vollständige Implementierung

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
    }

}

Verwendung

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

12
Sehr engagierte Antwort.
Hexfire

3
Anstelle von structIhnen enummit Schlüsseln verwendet. das ist viel eleganter 👍
Jack

1
Ein großes Dankeschön, dass Sie sich die Zeit genommen haben, dies so gut zu dokumentieren. Nachdem Sie so viele Dokumentationen zu Decodable durchsucht und JSON analysiert haben, hat Ihre Antwort wirklich viele Fragen geklärt, die ich hatte.
Marcy

30

Anstatt eine große CodingKeysAufzählung mit allen Schlüsseln zu haben, die Sie zum Dekodieren des JSON benötigen, würde ich empfehlen, die Schlüssel für jedes Ihrer verschachtelten JSON-Objekte aufzuteilen und verschachtelte Aufzählungen zu verwenden, um die Hierarchie beizubehalten:

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

Dies erleichtert das Verfolgen der Schlüssel auf jeder Ebene in Ihrem JSON.

Nun bedenken Sie Folgendes:

  • Ein Schlüsselcontainer wird zum Dekodieren eines JSON-Objekts verwendet und mit einem CodingKeykonformen Typ (wie den oben definierten) dekodiert .

  • Ein Container ohne Schlüssel wird zum Decodieren eines JSON-Arrays verwendet und nacheinander decodiert (dh jedes Mal, wenn Sie eine Decodierungs- oder verschachtelte Containermethode aufrufen, wird zum nächsten Element im Array übergegangen). Im zweiten Teil der Antwort erfahren Sie, wie Sie eine durchlaufen können.

Nachdem Sie Ihren Schlüsselcontainer der obersten Ebene mit container(keyedBy:)(da Sie ein JSON-Objekt auf der obersten Ebene haben) vom Decoder abgerufen haben, können Sie die folgenden Methoden wiederholt verwenden:

Beispielsweise:

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

Beispieldecodierung:

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)

Durch einen nicht verschlüsselten Behälter iterieren

Betrachten Sie den Fall, wo Sie wollen reviewCount eine sein [Int], wobei jedes Element den Wert darstellt , für den "count"Schlüssel in der verschachtelten JSON:

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

Sie müssen den verschachtelten Container ohne Schlüssel durchlaufen, den verschachtelten Container mit Schlüssel bei jeder Iteration abrufen und den Wert für den "count"Schlüssel dekodieren . Du kannst den ... benutzencount Eigenschaft des nicht verschlüsselten Containers verwenden, um das resultierende Array vorab zuzuweisen, und dann dasisAtEnd Eigenschaft, um es zu durchlaufen.

Beispielsweise:

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

eine Sache zu klären: Was hast du damit gemeint 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 Ich meine, anstatt eine große CodingKeysAufzählung mit allen Schlüsseln zu haben, die Sie zum Dekodieren Ihres JSON-Objekts benötigen, sollten Sie sie für jedes JSON-Objekt in mehrere Aufzählungen aufteilen - zum Beispiel im obigen Code, den wir CodingKeys.Usermit den Schlüsseln haben um das Benutzer-JSON-Objekt ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }) zu dekodieren , also nur die Schlüssel für "user_name"& "real_info".
Hamish

Vielen Dank. Sehr klare Antwort. Ich schaue immer noch durch, um es vollständig zu verstehen. Aber es funktioniert.
FlowUI. SimpleUITesting.com

Ich hatte eine Frage zu dem, reviews_countwas ein Array von Wörterbuch ist. Derzeit funktioniert der Code wie erwartet. Mein reviewsCount hat immer nur einen Wert im Array. Aber was wäre, wenn ich tatsächlich ein Array von review_count wollte, dann müsste ich es einfach var reviewCount: Intals Array deklarieren, oder? -> var reviewCount: [Int]. Und dann müsste ich auch die ReviewsCountAufzählung bearbeiten, oder?
FlowUI. SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift Das wäre tatsächlich etwas komplizierter, da das, was Sie beschreiben, nicht nur ein Array von Int, sondern ein Array von JSON-Objekten ist, die jeweils einen IntWert für einen bestimmten Schlüssel haben. Sie müssen also durchlaufen den nicht verschlüsselten Container und holen Sie sich alle verschachtelten Schlüsselcontainer, dekodieren Sie einen Intfür jeden (und hängen Sie
Hamish

4

Viele gute Antworten wurden bereits veröffentlicht, aber es gibt eine einfachere Methode, die IMO noch nicht beschrieben hat.

Wenn die JSON-Feldnamen mit geschrieben werden snake_case_notation, können Sie die camelCaseNotationin Ihrer Swift-Datei weiterhin verwenden.

Sie müssen nur einstellen

decoder.keyDecodingStrategy = .convertFromSnakeCase

Nach dieser Zeile Swift vergleicht Swift automatisch alle snake_caseFelder aus dem JSON mit den camelCaseFeldern im Swift-Modell.

Z.B

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

Hier ist der vollständige Code

1. Schreiben des Modells

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. Einstellen des Decoders

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

3. Dekodierung

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

1
Dies geht nicht auf die ursprüngliche Frage ein, wie mit verschiedenen Verschachtelungsebenen umgegangen werden soll.
Theo

2
  1. Kopieren Sie die JSON-Datei nach https://app.quicktype.io
  2. Wählen Sie Swift (wenn Sie Swift 5 verwenden, überprüfen Sie den Kompatibilitätsschalter für Swift 5).
  3. Verwenden Sie den folgenden Code, um die Datei zu dekodieren
  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
Hat für mich gearbeitet, danke. Diese Seite ist Gold. Wenn Viewer eine JSON-String-Variable dekodieren jsonStr, können Sie diese anstelle der beiden guard letoben genannten s verwenden: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }Konvertieren jsonStrDataSie sie dann in Ihre Struktur, wie oben in der let yourObjectZeile beschrieben
Fragen Sie P

Dies ist ein erstaunliches Werkzeug!
PostCodeism

0

Sie können auch die von mir vorbereitete Bibliothek KeyedCodable verwenden . Es wird weniger Code benötigt. Lassen Sie mich wissen, was Sie darüber denken.

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)
  }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.