Как декодировать вложенную структуру 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 по декодированию вложенных структур, но я все еще не понимаю, как правильно выполнять различные уровни JSON. Любая помощь будет высоко ценится.

9 ответов

Решение

Другой подход заключается в создании промежуточной модели, которая точно соответствует JSON (с помощью такого инструмента, как https://app.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 значения в будущем.

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


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

Вместо того, чтобы иметь один большой 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'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
    }
}

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

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

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

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

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

  • nestedContainer(keyedBy:forKey:) получить вложенный объект из объекта по заданному ключу
  • nestedUnkeyedContainer(forKey:) получить вложенный массив из объекта по заданному ключу
  • nestedContainer(keyedBy:) получить следующий вложенный объект из массива
  • nestedUnkeyedContainer() получить следующий вложенный массив из массива

Например:

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)
            )
        }
    }
}
  1. Скопируйте файл json на https://app.quicktype.io/
  2. Выберите Swift (если вы используете Swift 5, проверьте переключатель совместимости для Swift 5)
  3. Используйте следующий код для декодирования файла
  4. Вуаля!
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)

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

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

Уже было опубликовано много хороших ответов, но есть более простой метод, который еще не описан, 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)
}

Вам не нужно многого, как эти гигантские ответы!

Вам не нужно объяснять себя. Swift может сделать почти все автоматически.

✅ Используйте простое расширение для вашей карты:

      extension Body {
    var username: String { user.userName }
    var fullName: String { user.realInfo.fullName }
    var reviewCount: Int { reviewsCount.first?.count ?? 0 }
}

Вам просто нужно определить вложенные объекты «вложенно» так, как это делает API:

      struct Body: Codable {
    let id: Int
    private let user: User
    private let reviewsCount: [ReviewCount]

    struct User: Codable {
        let userName: String
        let realInfo: RealInfo

        struct RealInfo: Codable {
            let fullName: String
        }
    }

    struct ReviewCount: Codable {
        let count: Int
    }
}

и декодируем его декодером с правильнымkeyDecodingStrategy:

      let jsonDecoder: JSONDecoder = {
    var decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase //  this will take care of the key namings
    return decoder
}()

Пояснительный ответ:

Рассматриваемая проблема заключается в следующем:

«Как декодировать вложенную структуру JSON с помощью Swift…»

На момент написания в Swift этого нет :

      struct Person: Codable {
    let height: String
    let hair: String
    let name: {
                let first: String
                let last: String
              }
}

Вы просто вводите это:

      struct Person: Codable {
    let height: String
    let hair: String
    let name: Name
}
struct Name: Codable {
    let first: String
    let last: String
}

Вот и все.

Также вы можете использовать библиотеку 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)
  }
}
Другие вопросы по тегам