Swift Decodableプロトコルを使用してネストされたJSON構造体をデコードする方法は?


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を適切に実行する方法がまだわかりません。どんな助けでも大歓迎です。

回答:


109

もう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、将来的に複数の値が含まれる場合に、を簡単に繰り返すことができます。


OK。このアプローチは非常にきれいに見えます。私の場合のために、私はそれを使用すると思う
FlowUIを。SimpleUITesting.com 2017年

ええ、私は間違いなくこれを考えすぎました– @JTAppleCalendarforiOSSwiftそれはより良い解決策なので、あなたはそれを受け入れるべきです。
ハミッシュ2017年

@ハミッシュわかりました。切り替えましたが、非常に詳細な回答でした。私はそれから多くを学びました。
FlowUI。SimpleUITesting.com 2017年

どうすれば実装できるのか知りたい EncodableServerResponse同じアプローチに従って構造をする。それも可能ですか?
nayem

1
@nayem問題は、ServerResponseデータがRawServerResponse。より少ないことです。RawServerResponseインスタンスをキャプチャし、からのプロパティで更新してServerResponse、そこからJSONを生成できます。あなたが直面している特定の問題について新しい質問を投稿することで、より良い助けを得ることができます。
コードが異なる

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 2017

3
キーでstruct使用enumする代わりに。これははるかにエレガントです👍–
ジャック

1
これをうまく文書化するために時間を割いてくれてありがとう。DecodableとJSONの解析に関する多くのドキュメントを精査した後、あなたの答えは私が持っていた多くの質問を本当にクリアしました。
マーシー

30

JSONのデコードに必要なすべてのキーを含む1つの大きなCodingKeys列挙を作成するのではなく、ネストされた列挙を使用して階層を保持し、ネストされた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配列をデコードするために使用され、デコードされる順番(すなわち、あなたがそれにデコードまたはネストされたコンテナのメソッドを呼び出すたびに、それが配列の次の要素に進みます)。1つを反復処理する方法については、回答の2番目の部分を参照してください。

(トップレベルにJSONオブジェクトがあるため)デコーダーからトップレベルのキー付きコンテナーを取得した後container(keyedBy:)、次のメソッドを繰り返し使用できます。

例えば:

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つのこと:あなたはどういう意味 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 2017年

@JTAppleCalendarforiOSSwiftつまり、JSONオブジェクトをデコードするために必要なすべてのキーを含む1つの大きなCodingKeys列挙型を用意するのではなく、JSONオブジェクトごとに複数の列挙型に分割する必要があります。たとえば、上記のコードでは、キーを使用しています。ユーザーのJSONオブジェクト()をデコードするため、&のキーのみ。CodingKeys.User{ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }"user_name""real_info"
ハミッシュ2017年

ありがとう。非常に明確な応答。私はまだそれを完全に理解するためにそれを調べています。しかし、それは機能します。
FlowUI。SimpleUITesting.com 2017年

reviews_countどれが辞書の配列であるかについて1つの質問がありました。現在、コードは期待どおりに機能します。私のreviewsCountは、配列に1つの値しかありません。しかし、実際にreview_countの配列が必要な場合は、単純にvar reviewCount: Int配列として宣言する必要がありますか?-> var reviewCount: [Int]。そして、ReviewsCount列挙型も編集する必要がありますか?
FlowUI。SimpleUITesting.com 2017年

1
@JTAppleCalendarforiOSSwift説明しているのは、の配列だけIntでなく、それぞれがInt特定のキーの値を持つJSONオブジェクトの配列であるため、実際には少し複雑になります。したがって、実行する必要があるのは、繰り返し処理することです。キーなしコンテナを取得し、ネストされたキー付きコンテナをすべて取得して、Intそれぞれをデコードします(次に、それらを配列に追加します)。例:gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
Hamish

4

多くの良い答えがすでに投稿されていますが、IMOにはまだ説明されていないより簡単な方法があります。

JSONフィールド名がを使用して記述されているsnake_case_notation場合でもcamelCaseNotation、Swiftファイルでを使用できます。

設定する必要があります

decoder.keyDecodingStrategy = .convertFromSnakeCase

この☝️行の後、Swiftはsnake_caseJSONのすべてのフィールドをcamelCaseSwiftモデルのフィールドに自動的に一致させます。

例えば

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上記の2つの代わりにこれを使用できます。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)
  }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.