Comment décoder une structure JSON imbriquée avec le protocole Swift Decodable?

90

Voici mon JSON

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

Voici la structure dans laquelle je veux l'enregistrer (incomplète)

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

J'ai regardé la documentation d'Apple sur le décodage des structures imbriquées, mais je ne comprends toujours pas comment faire correctement les différents niveaux du JSON. Toute aide sera très appréciée.

FlowUI. SimpleUITesting.com
la source

Réponses:

109

Une autre approche consiste à créer un modèle intermédiaire qui correspond étroitement au JSON (à l'aide d'un outil comme quicktype.io ), à laisser Swift générer les méthodes pour le décoder, puis à sélectionner les éléments que vous souhaitez dans votre modèle de données final:

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

Cela vous permet également d'itérer facilement reviews_count, si elle contient plus d'une valeur à l'avenir.

Code différent
la source
D'accord. cette approche semble très propre. Pour mon cas, je pense que je vais l'utiliser
FlowUI. SimpleUITesting.com
Oui, j'ai vraiment trop réfléchi à cela - @JTAppleCalendarforiOSSwift, vous devriez l'accepter, car c'est une meilleure solution.
Hamish
@Hamish ok. Je l'ai changé, mais votre réponse était extrêmement détaillée. J'en ai beaucoup appris.
FlowUI. SimpleUITesting.com
Je suis curieux de savoir comment on peut mettre en œuvre Encodablepour la ServerResponsestructure en suivant la même approche. Est-ce même possible?
nayem
1
@nayem le problème est qu'il ServerResponsea moins de données que RawServerResponse. Vous pouvez capturer l' RawServerResponseinstance, la mettre à jour avec les propriétés de ServerResponse, puis générer le JSON à partir de cela. Vous pouvez obtenir une meilleure aide en publiant une nouvelle question avec le problème spécifique auquel vous êtes confronté.
Code différent
95

Afin de résoudre votre problème, vous pouvez diviser votre RawServerResponseimplémentation en plusieurs parties logiques (en utilisant Swift 5).


#1. Implémenter les propriétés et les clés de codage requises

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. Définir la stratégie de décodage pour la idpropriété

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. Définir la stratégie de décodage pour la userNamepropriété

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. Définir la stratégie de décodage pour la fullNamepropriété

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. Définir la stratégie de décodage pour la reviewCountpropriété

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
    }

}

Mise en œuvre complète

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
    }

}

Usage

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
la source
13
Réponse très dédiée.
Hexfire
3
Au lieu de structvous utilisé enumavec des clés. ce qui est beaucoup plus élégant 👍
Jack
1
Un immense merci d'avoir mis le temps de bien documenter cela. Après avoir parcouru autant de documentation sur Decodable et analysé JSON, votre réponse a vraiment éclairci de nombreuses questions que j'avais.
Marcy
30

Plutôt que d'avoir une grande CodingKeysénumération avec toutes les clés dont vous aurez besoin pour décoder le JSON, je vous conseillerais de diviser les clés pour chacun de vos objets JSON imbriqués, en utilisant des énumérations imbriquées pour préserver la hiérarchie:

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

Cela facilitera le suivi des clés à chaque niveau de votre JSON.

Maintenant, en gardant à l'esprit que:

  • Un conteneur à clé est utilisé pour décoder un objet JSON et est décodé avec un CodingKeytype conforme (tel que ceux que nous avons définis ci-dessus).

  • Un conteneur sans clé est utilisé pour décoder un tableau JSON et est décodé séquentiellement (c'est-à-dire que chaque fois que vous appelez une méthode de décodage ou de conteneur imbriqué dessus, il passe à l'élément suivant du tableau). Voir la deuxième partie de la réponse pour savoir comment vous pouvez en parcourir une.

Après avoir obtenu votre conteneur à clé de niveau supérieur du décodeur avec container(keyedBy:)(comme vous avez un objet JSON au niveau supérieur), vous pouvez utiliser à plusieurs reprises les méthodes:

Par exemple:

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

Exemple de décodage:

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)

Itérer dans un conteneur sans clé

Considérant le cas où vous voulez reviewCountêtre an [Int], où chaque élément représente la valeur de la "count"clé dans le JSON imbriqué:

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

Vous devrez parcourir le conteneur imbriqué sans clé, obtenir le conteneur à clé imbriqué à chaque itération et décoder la valeur de la "count"clé. Vous pouvez utiliser la countpropriété du conteneur sans clé afin de pré-allouer le tableau résultant, puis la isAtEndpropriété pour l'itérer.

Par exemple:

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
la source
une chose à clarifier: qu'entendez-vous par 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 Je veux dire qu'au lieu d'avoir une grande CodingKeysénumération avec toutes les clés dont vous aurez besoin pour décoder votre objet JSON, vous devriez les diviser en plusieurs énumérations pour chaque objet JSON - par exemple, dans le code ci-dessus que nous avons CodingKeys.Useravec les clés pour décoder l'objet JSON utilisateur ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), donc juste les clés pour "user_name"& "real_info".
Hamish
Merci. Réponse très claire. Je regarde toujours à travers pour le comprendre pleinement. Mais ça marche.
FlowUI. SimpleUITesting.com
J'avais une question sur le reviews_countqui est un tableau de dictionnaire. Actuellement, le code fonctionne comme prévu. My reviewsCount n'a jamais qu'une seule valeur dans le tableau. Mais que faire si je voulais réellement un tableau de review_count, alors je devrais simplement le déclarer var reviewCount: Intcomme un tableau, n'est-ce pas? -> var reviewCount: [Int]. Et puis j'aurais besoin de modifier également l' ReviewsCounténumération, n'est-ce pas?
FlowUI. SimpleUITesting.com
1
@JTAppleCalendarforiOSSwift Ce serait en fait un peu plus compliqué, car ce que vous décrivez n'est pas seulement un tableau Int, mais un tableau d'objets JSON qui ont chacun une Intvaleur pour une clé donnée - donc ce que vous devez faire est de parcourir le conteneur sans clé et obtenez tous les conteneurs à clé imbriqués, en décodant un Intpour chacun (puis en les ajoutant à votre tableau), par exemple gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish
4

De nombreuses bonnes réponses ont déjà été postées, mais il existe une méthode plus simple non encore décrite à l'OMI.

Lorsque les noms de champ JSON sont écrits à l'aide de, snake_case_notationvous pouvez toujours utiliser le camelCaseNotationdans votre fichier Swift.

Vous avez juste besoin de définir

decoder.keyDecodingStrategy = .convertFromSnakeCase

Après cette ligne ☝️, Swift fera automatiquement correspondre tous les snake_casechamps du JSON aux camelCasechamps du modèle Swift.

Par exemple

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

Voici le code complet

1. Rédaction du modèle

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. Réglage du décodeur

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

3. Décodage

do {
    let response = try? decoder.decode(Response.self, from: data)
    print(response)
} catch {
    debugPrint(error)
}
Luca Angeletti
la source
2
Cela ne répond pas à la question initiale de savoir comment gérer les différents niveaux d'imbrication.
Theo
2
  1. Copiez le fichier json sur https://app.quicktype.io
  2. Sélectionnez Swift (si vous utilisez Swift 5, vérifiez le commutateur de compatibilité pour Swift 5)
  3. Utilisez le code suivant pour décoder le fichier
  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
la source
1
A travaillé pour moi, merci. Ce site est en or. Pour les téléspectateurs, si vous décodez une variable de chaîne json jsonStr, vous pouvez l'utiliser à la place des deux guard lets ci-dessus: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }puis convertissez- jsonStrDatavous en votre structure comme décrit ci-dessus sur la let yourObjectligne
Demandez P
C'est un outil incroyable!
PostCodeism
0

Vous pouvez également utiliser la bibliothèque KeyedCodable que j'ai préparée. Cela nécessitera moins de code. Dis moi ce que tu penses de ça.

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)
  }
}
decybel
la source