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


90

Вот мой 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. Любая помощь будет высоко ценится.

Ответы:


110

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


ОК. этот подход выглядит очень чистым. В моем случае, думаю, воспользуюсь
FlowUI. SimpleUITesting.com

Да, я определенно переоценил это - @JTAppleCalendarforiOSSwift, вы должны принять это, так как это лучшее решение.
Хэмиш

@ Хамиш, ладно. Я переключил его, но ваш ответ был очень подробным. Я многому у него научился.
FlowUI. SimpleUITesting.com

Я хотел бы знать , как можно реализовать Encodableдля ServerResponseструктуры следующего такого же подхода. Это вообще возможно?
найем

1
@nayem проблема в том, ServerResponseчто данных меньше, чем RawServerResponse. Вы можете захватить RawServerResponseэкземпляр, обновить его свойствами из ServerResponse, а затем сгенерировать из него JSON. Вы можете получить лучшую помощь, разместив новый вопрос с конкретной проблемой, с которой вы столкнулись.
Code Different

95

Чтобы решить вашу проблему, вы можете разбить вашу 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
*/

13
Очень честный ответ.
Hexfire

3
Вместо того, structчтобы использовать enumс ключами. что намного элегантнее 👍
Джек

1
Огромное спасибо за то, что нашли время так хорошо задокументировать это. После тщательного изучения документации по Decodable и анализа JSON ваш ответ действительно прояснил многие мои вопросы.
Марси

30

Вместо того, чтобы иметь одно большое 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соответствующим типом (например , как те , мы определили выше).

  • Unkeyed контейнер используется для декодирования массива 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)
            )
        }
    }
}

одно прояснить: что вы имели в виду 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 Я имею в виду, что вместо того, чтобы иметь одно большое CodingKeysперечисление со всеми ключами, которые вам понадобятся для декодирования вашего объекта JSON, вы должны разделить их на несколько перечислений для каждого объекта JSON - например, в приведенном выше коде у нас есть CodingKeys.Userключи для декодирования пользовательского объекта JSON ( { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }), поэтому просто ключи для "user_name"& "real_info".
Хэмиш

Спасибо. Очень четкий ответ. Я все еще просматриваю это, чтобы полностью понять. Но это работает.
FlowUI. SimpleUITesting.com

У меня был один вопрос о том, reviews_countчто представляет собой массив словаря. В настоящее время код работает должным образом. My reviewsCount всегда имеет только одно значение в массиве. Но что, если мне действительно нужен массив review_count, тогда мне нужно будет просто объявить var reviewCount: Intкак массив, правильно? -> var reviewCount: [Int]. И тогда мне нужно будет также отредактировать ReviewsCountперечисление, верно?
FlowUI. SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift На самом деле это было бы немного сложнее, поскольку то, что вы описываете, - это не просто массив Int, а массив объектов JSON, каждый из которых имеет Intзначение для данного ключа, поэтому вам нужно выполнить итерацию контейнер без ключа и получите все вложенные контейнеры с ключами, декодируя Intдля каждого из них (а затем добавляя их в свой массив), например gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Хэмиш

4

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

2
Это не решает исходный вопрос, как бороться с разными уровнями вложенности.
Тео

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

1
Сработало у меня, спасибо. Этот сайт золотой. Для зрителей, если вы декодируете строковую переменную json jsonStr, вы можете использовать это вместо двух guard lets выше: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }затем преобразовать jsonStrDataв свою структуру, как описано выше в let yourObjectстроке
Ask P

Это потрясающий инструмент!
PostCodeism

0

Также вы можете использовать подготовленную мной библиотеку 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)
  }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.