Come decodificare una struttura JSON nidificata con il protocollo Swift Decodable?


90

Ecco il mio JSON

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

Ecco la struttura in cui voglio che venga salvato (incompleto)

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

Ho esaminato la documentazione di Apple sulla decodifica di strutture nidificate, ma ancora non capisco come eseguire correttamente i diversi livelli di JSON. Qualsiasi aiuto sarà molto apprezzato.

Risposte:


109

Un altro approccio consiste nel creare un modello intermedio che corrisponda strettamente al JSON (con l'aiuto di uno strumento come quicktype.io ), lasciare che Swift generi i metodi per decodificarlo e quindi scegliere i pezzi che desideri nel modello dati finale:

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

Questo ti consente anche di iterare facilmente reviews_count, se dovesse contenere più di 1 valore in futuro.


Ok. questo approccio sembra molto pulito. Per il mio caso, penso che lo userò
FlowUI. SimpleUITesting.com

Sì, ho decisamente ripensato a questo - @JTAppleCalendarforiOSSwift dovresti accettarlo, poiché è una soluzione migliore.
Hamish

@ Hamish ok. L'ho cambiato, ma la tua risposta è stata estremamente dettagliata. Ho imparato molto da questo.
FlowUI. SimpleUITesting.com

Sono curioso di sapere come si possa implementare Encodableper la ServerResponsestruttura seguendo lo stesso approccio. È anche possibile?
nayem

1
@nayem il problema è che ServerResponseha meno dati di RawServerResponse. Puoi acquisire l' RawServerResponseistanza, aggiornarla con le proprietà da ServerResponse, quindi generare il JSON da quella. Puoi ottenere un aiuto migliore pubblicando una nuova domanda con il problema specifico che stai affrontando.
Code Different

95

Per risolvere il tuo problema, puoi suddividere la tua RawServerResponseimplementazione in più parti logiche (usando Swift 5).


# 1. Implementa le proprietà e le chiavi di codifica richieste

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. Imposta la strategia di decodifica per la idproprietà

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. Imposta la strategia di decodifica per la userNameproprietà

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. Imposta la strategia di decodifica per la fullNameproprietà

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. Imposta la strategia di decodifica per la reviewCountproprietà

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
    }

}

Completa implementazione

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
    }

}

Utilizzo

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

13
Risposta molto dedicata.
Hexfire

3
Invece di structessere usato enumcon le chiavi. che è molto più elegante 👍
Jack

1
Un enorme grazie per aver dedicato del tempo per documentarlo così bene. Dopo aver setacciato così tanta documentazione su Decodable e aver analizzato JSON, la tua risposta ha davvero chiarito molte domande che avevo.
Marcy

30

Piuttosto che avere una grande CodingKeysenumerazione con tutte le chiavi necessarie per decodificare il JSON, consiglierei di suddividere le chiavi per ciascuno dei tuoi oggetti JSON nidificati, utilizzando enumerazioni nidificate per preservare la gerarchia:

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

Ciò renderà più facile tenere traccia delle chiavi a ogni livello nel tuo JSON.

Ora, tenendo presente che:

  • Un contenitore con chiave viene utilizzato per decodificare un oggetto JSON e viene decodificato con un CodingKeytipo conforme (come quelli che abbiamo definito sopra).

  • Un contenitore senza chiave viene utilizzato per decodificare un array JSON e viene decodificato in sequenza (ovvero ogni volta che si chiama un metodo contenitore decodificato o annidato su di esso, si passa all'elemento successivo dell'array). Vedi la seconda parte della risposta per scoprire come puoi iterarne una.

Dopo aver ottenuto il contenitore con chiave di primo livello dal decodificatore con container(keyedBy:)(dato che hai un oggetto JSON al livello superiore), puoi utilizzare ripetutamente i metodi:

Per esempio:

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

Esempio di decodifica:

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)

Iterazione attraverso un contenitore senza chiave

Considerando il caso in cui si desidera reviewCountessere un [Int], in cui ogni elemento rappresenta il valore per la "count"chiave nel JSON nidificato:

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

Dovrai iterare attraverso il contenitore senza chiave nidificato, ottenere il contenitore con chiave nidificato a ogni iterazione e decodificare il valore per la "count"chiave. È possibile utilizzare la countproprietà del contenitore senza chiave per pre-allocare la matrice risultante, quindi la isAtEndproprietà per iterare attraverso di essa.

Per esempio:

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

una cosa da chiarire: cosa intendevi con 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 Voglio dire che invece di avere una grande CodingKeysenumerazione con tutte le chiavi di cui avrai bisogno per decodificare il tuo oggetto JSON, dovresti dividerle in più enumerazioni per ogni oggetto JSON, ad esempio, nel codice sopra che abbiamo CodingKeys.Usercon le chiavi per decodificare l'oggetto JSON dell'utente ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), quindi solo le chiavi per "user_name"& "real_info".
Hamish

Grazie. Risposta molto chiara. Sto ancora esaminandolo per capirlo appieno. Ma funziona.
FlowUI. SimpleUITesting.com

Avevo una domanda su reviews_countche è una matrice di dizionario. Attualmente, il codice funziona come previsto. My reviewsCount ha sempre e solo un valore nell'array. Ma cosa succede se in realtà volessi un array di review_count, allora dovrei semplicemente dichiarare var reviewCount: Intcome un array giusto? -> var reviewCount: [Int]. E poi dovrei modificare anche l' ReviewsCountenumerazione, giusto?
FlowUI. SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift Sarebbe in realtà leggermente più complicato, poiché ciò che stai descrivendo non è solo un array di Int, ma un array di oggetti JSON che hanno ciascuno un Intvalore per una determinata chiave, quindi ciò che dovresti fare è iterare attraverso il contenitore senza chiave e ottieni tutti i contenitori con chiave nidificati, decodificandone uno Intper ciascuno (e quindi aggiungendoli al tuo array), ad esempio gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

Molte buone risposte sono già state pubblicate, ma c'è un metodo più semplice non ancora descritto IMO.

Quando i nomi dei campi JSON vengono scritti utilizzando snake_case_notationè ancora possibile utilizzare ilcamelCaseNotation nel tuo file Swift.

Hai solo bisogno di impostare

decoder.keyDecodingStrategy = .convertFromSnakeCase

Dopo questa ☝️ linea Swift abbinerà automaticamente tutti i snake_casecampi dal JSON ai camelCasecampi nel modello Swift.

Per esempio

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

Ecco il codice completo

1. Scrittura del modello

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. Impostazione del decoder

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

3. Decodifica

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

2
Questo non risolve la domanda originale su come gestire i diversi livelli di nidificazione.
Theo

2
  1. Copia il file json su https://app.quicktype.io
  2. Seleziona Swift (se usi Swift 5, controlla l'interruttore di compatibilità per Swift 5)
  3. Usa il codice seguente per decodificare il file
  4. Ecco!
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
Ha funzionato per me, grazie. Quel sito è d'oro. Per i visualizzatori, se decodifica una variabile stringa json jsonStr, puoi usarla al posto delle due guard letsopra: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }quindi converti jsonStrDatanella tua struttura come descritto sopra alla let yourObjectriga
Chiedi P

Questo è uno strumento fantastico!
PostCodeism,

0

Inoltre puoi usare la libreria KeyedCodable che ho preparato. Richiederà meno codice. Fammi sapere cosa ne pensi.

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)
  }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.