Як декодувати вкладену структуру JSON за допомогою протоколу Swift Decodable?


91

Ось мій 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. Будь-яка допомога буде дуже вдячна.

Відповіді:


111

Інший підхід полягає у створенні проміжної моделі, яка тісно відповідає 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, ви повинні це прийняти, оскільки це краще рішення.
Хаміш,

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

Я хотів би знати , як можна реалізувати Encodableдля ServerResponseструктури наступного такого ж підходу. Це взагалі можливо?
nayem

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

96

Для того, щоб вирішити вашу проблему, ви можете розділити свою 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)

        /* ... */
    }

}

No4. Встановіть стратегію декодування для 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який це масив словника. В даний час код працює як слід. Мої оглядиCount лише колись мають одне значення в масиві. Але що, якби я насправді хотів масив 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 letвищезазначених: guard let jsonStrData: Data? = jsonStr.data(using: .utf8)! else { print("error") }потім перетворіть jsonStrDataу свою структуру, як описано вище на let yourObjectрядку
Запитайте 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.