Como decodificar uma estrutura JSON aninhada com o protocolo Swift Decodable?


94

Aqui está meu JSON

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

Aqui está a estrutura que eu quero salvar (incompleta)

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

Eu olhei a documentação da Apple sobre a decodificação de structs aninhados, mas ainda não entendi como fazer os diferentes níveis do JSON corretamente. Qualquer ajuda será muito apreciada.

Respostas:


113

Outra abordagem é criar um modelo intermediário que corresponda de perto ao JSON (com a ajuda de uma ferramenta como quicktype.io ), deixar o Swift gerar os métodos para decodificá-lo e, em seguida, selecionar as partes que você deseja em seu modelo de dados 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
    }
}

Isso também permite que você itere facilmente reviews_count, caso contenha mais de 1 valor no futuro.


Está bem. essa abordagem parece muito limpa. Para o meu caso, acho que vou usá-lo
apenas um codificador

Sim, definitivamente pensei demais sobre isso - @JTAppleCalendarforiOSSwift, você deve aceitá-lo, pois é uma solução melhor.
Hamish de

@Hamish ok. troquei, mas sua resposta foi extremamente detalhada. Eu aprendi muito com isso.
Apenas um codificador de

Estou curioso para saber como implementar Encodablea ServerResponseestrutura seguindo a mesma abordagem. É mesmo possível?
nayem

1
@nayem o problema é que ServerResponsetem menos dados do que RawServerResponse. Você pode capturar a RawServerResponseinstância, atualizá-la com propriedades de e ServerResponse, em seguida, gerar o JSON a partir disso. Você pode obter ajuda melhor postando uma nova pergunta com o problema específico que está enfrentando.
Código diferente de

100

Para resolver seu problema, você pode dividir sua RawServerResponseimplementação em várias partes lógicas (usando Swift 5).


# 1. Implementar as propriedades e as chaves de codificação necessárias

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. Defina a estratégia de decodificação para idpropriedade

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. Defina a estratégia de decodificação da userNamepropriedade

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. Defina a estratégia de decodificação da fullNamepropriedade

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. Defina a estratégia de decodificação para reviewCountpropriedade

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
    }

}

Implementação completa

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
    }

}

Uso

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
Resposta muito dedicada.
Hexfire

3
Em vez de structvocê usar enumcom as chaves. que é muito mais elegante 👍
Jack

1
Muito obrigado por dedicar seu tempo para documentar isso tão bem. Depois de vasculhar tanta documentação sobre Decodable e parsing JSON, sua resposta realmente esclareceu muitas dúvidas que eu tinha.
Marcy

31

Em vez de ter uma grande CodingKeysenumeração com todas as chaves de que você precisará para decodificar o JSON, aconselho dividir as chaves para cada um de seus objetos JSON aninhados, usando enumerações aninhadas para preservar a hierarquia:

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

Isso tornará mais fácil controlar as chaves em cada nível do JSON.

Agora, tendo em mente que:

  • Um contêiner com chave é usado para decodificar um objeto JSON e é decodificado com um CodingKeytipo em conformidade (como os que definimos acima).

  • Um contêiner sem chave é usado para decodificar uma matriz JSON e é decodificado sequencialmente (ou seja, cada vez que você chama um método de decodificação ou contêiner aninhado nele, ele avança para o próximo elemento na matriz). Veja a segunda parte da resposta para saber como você pode iterar em um.

Depois de obter seu contêiner codificado de nível superior do decodificador com container(keyedBy:)(já que você tem um objeto JSON no nível superior), você pode usar os métodos repetidamente:

Por exemplo:

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

Exemplo de decodificação:

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)

Iterando por meio de um contêiner sem chave

Considerando o caso em que você deseja reviewCountser um [Int], em que cada elemento representa o valor da "count"chave no JSON aninhado:

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

Você precisará iterar pelo contêiner aninhado sem chave, obtendo o contêiner aninhado com chave em cada iteração e decodificando o valor da "count"chave. Você pode usar a countpropriedade do contêiner sem chave para pré-alocar a matriz resultante e, em seguida, a isAtEndpropriedade para iterar por meio dela.

Por exemplo:

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

uma coisa a esclarecer: o que você quis dizer com 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?
Apenas um codificador de

@JTAppleCalendarforiOSSwift quero dizer que, em vez de ter um grande CodingKeysenum com todas as chaves que você precisará para decodificar seu objeto JSON, você deve dividi-los em vários enums para cada objeto JSON - por exemplo, no código acima que temos CodingKeys.Usercom as chaves para decodificar o objeto JSON do usuário ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), então apenas as chaves para "user_name"& "real_info".
Hamish de

Obrigado. Resposta muito clara. Ainda estou procurando para entendê-lo completamente. Mas funciona.
Apenas um codificador de

Tive uma pergunta sobre o reviews_countque é um dicionário. Atualmente, o código funciona conforme o esperado. Meu reviewsCount sempre tem apenas um valor na matriz. Mas e se eu realmente quisesse um array de review_count, então precisaria simplesmente declarar var reviewCount: Intcomo um array, certo? -> var reviewCount: [Int]. E então eu preciso editar o ReviewsCountenum, certo?
Apenas um codificador de

1
@JTAppleCalendarforiOSSwift Isso seria um pouco mais complicado, pois o que você está descrevendo não é apenas uma matriz Int, mas uma matriz de objetos JSON em que cada um tem um Intvalor para uma determinada chave - então o que você precisa fazer é iterar através o contêiner sem chave e obter todos os contêineres com chave aninhados, decodificando um Intpara cada um (e depois anexando-os à sua matriz), por exemplo, gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

Muitas boas respostas já foram postadas, mas existe um método mais simples ainda não descrito na IMO.

Quando os nomes de campo JSON são gravados usando, snake_case_notationvocê ainda pode usar camelCaseNotationno seu arquivo Swift.

Você só precisa definir

decoder.keyDecodingStrategy = .convertFromSnakeCase

Após essa ☝️ linha, o Swift fará a correspondência automática de todos os snake_casecampos do JSON com os camelCasecampos do modelo Swift.

Por exemplo

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

Aqui está o código completo

1. Escrevendo o modelo

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. Configurando o decodificador

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

3. Decodificação

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

2
Isso não aborda a questão original de como lidar com diferentes níveis de aninhamento.
Theo

3
  1. Copie o arquivo json para https://app.quicktype.io
  2. Selecione Swift (se você usa Swift 5, verifique a chave de compatibilidade para Swift 5)
  3. Use o seguinte código para decodificar o arquivo
  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
Funcionou para mim, obrigado. Esse site é ouro. Para visualizadores, se decodificar uma variável de string json jsonStr, você pode usar isso em vez dos dois guard lets acima: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }então converta jsonStrDatapara sua estrutura conforme descrito acima na let yourObjectlinha
Pergunte P

Esta é uma ferramenta incrível!
PostCodeism

0

Você também pode usar a biblioteca KeyedCodable que preparei. Exigirá menos código. Deixe-me saber o que você pensa sobre isso.

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)
  }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.