Подтвердить что ты не робот

Как декодировать вложенную структуру JSON с протоколом Swift Decodable?

Вот мой JSON

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

Вот структура, которую я хочу сохранить (неполным)

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

Я рассмотрел Apple Documentation об декодировании вложенных структур, но я до сих пор не понимаю, как правильно выполнять различные уровни JSON. Любая помощь будет высоко оценена.

4b9b3361

Ответ 1

Другой подход заключается в создании промежуточной модели, которая точно соответствует JSON (с помощью такого инструмента, как quicktype.io), пусть Swift генерирует методы для его декодирования, а затем отбирает фрагменты, которые вы хотите в своей окончательной модели данных:

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

Это также позволяет вам легко выполнять итерации по reviews_count, если в будущем оно будет содержать более 1 значения.

Ответ 2

Чтобы решить вашу проблему, вы можете разделить реализацию RawServerResponse на несколько логических частей (используя Swift 5).


# 1. Реализуйте свойства и необходимые ключи кодирования

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. Установите стратегию декодирования для свойства 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. Установите стратегию декодирования для свойства 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. Установите стратегию декодирования для свойства 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. Установите стратегию декодирования для свойства 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
    }

}

Полная реализация

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
    }

}

использование

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

Ответ 3

Вместо того, чтобы иметь одно большое перечисление CodingKeys со всеми ключами, необходимыми для декодирования JSON, я бы посоветовал разделить ключи для каждого из ваших вложенных объектов JSON, используя вложенные перечисления для сохранения иерархии:

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

Это упростит отслеживание ключей на каждом уровне вашего JSON.

Теперь, имея в виду, что:

  • A keyed container используется для декодирования объекта JSON и декодируется с помощью CodingKey соответствующий тип (например, те, которые мы определили выше).

  • unkeyed container используется для декодирования массива JSON и декодируется последовательно (т.е. каждый раз, когда вы вызываете декодирование или вложенный контейнер на нем, он переходит к следующему элементу массива). См. Вторую часть ответа о том, как вы можете проходить через один.

После получения контейнера с ключом верхнего уровня из декодера с container(keyedBy:) (так как у вас есть объект JSON на верхнем уровне), вы можете неоднократно использовать методы:

Например:

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

Пример декодирования:

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)

Итерация через контейнер без ключа

Учитывая случай, когда вы хотите reviewCount быть [Int], где каждый элемент представляет значение для ключа "count" вложенного JSON:

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

Вам потребуется выполнить итерацию через вложенный контейнер без ключа, получить контейнер с вложенными ключами на каждой итерации и декодировать значение для ключа "count". Вы можете использовать свойство count для контейнера без ключа, чтобы предварительно выделить результирующий массив, а затем isAtEnd, чтобы перебирать его.

Например:

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

Ответ 4

Эти ребята уже ответили на мой вопрос, но я просто подумал, что выложу здесь эту ссылку, которая значительно облегчит эту задачу → https://app.quicktype.io/#l=swift

Просто опубликуйте свой ответ JSON на левой панели и посмотрите, как сгенерированы ваши модели справа. Это может помочь, когда вы только начинаете.

Ответ 5

Также вы можете использовать библиотеку KeyedCodable, которую я подготовил. Это потребует меньше кода. Дай мне знать, что ты думаешь об этом.

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

Ответ 6

Многие хорошие ответы уже были опубликованы, но есть более простой метод, который еще не описан в IMO.

Когда имена полей JSON пишутся с использованием snake_case_notation вы все равно можете использовать camelCaseNotation в вашем файле Swift.

Вам просто нужно установить

decoder.keyDecodingStrategy = .convertFromSnakeCase

После этой ☝️ строки Swift автоматически сопоставит все поля snake_case из JSON с полями camelCase в модели Swift.

Например

user_name' -> userName
reviews_count -> 'reviewsCount
...

Здесь полный код

1. Написание модели

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. Настройка декодера

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

3. Расшифровка

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