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

또 다른 접근 방식은 ( quicktype.io 와 같은 도구를 사용하여) JSON과 거의 일치하는 중간 모델을 만들고 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나중에 둘 이상의 값을 포함 할 경우을 통해 쉽게 반복 할 수 있습니다 .


확인. 이 접근 방식은 매우 깔끔해 보입니다. 내 경우, 나는 내가 그것을 사용하는 것 같아요
FlowUI합니다. SimpleUITesting.com

그래, 나는 이것을 확실히 과장했다 – @JTAppleCalendarforiOSSwift 더 나은 해결책이기 때문에 당신은 그것을 받아 들여야한다.
Hamish

@Hamish 좋아요. 나는 그것을 바꾸었지만 당신의 대답은 매우 상세했습니다. 나는 그것으로부터 많은 것을 배웠다.
FlowUI. SimpleUITesting.com

동일한 접근 방식 Encodable에 따라 ServerResponse구조를 구현할 수있는 방법을 알고 싶습니다 . 가능할까요?
nayem

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)

        /* ... */                 
    }

}

#삼. 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하는 대신 . 훨씬 더 우아합니다 👍
Jack

1
시간을내어 이것을 잘 문서화 해 주셔서 대단히 감사합니다. Decodable에 대한 많은 문서를 샅샅이 뒤지고 JSON을 구문 분석 한 후 귀하의 답변은 실제로 내가 가진 많은 질문을 해결했습니다.
Marcy

30

JSON 디코딩에 필요한 모든 키 가 포함 된 하나의 큰 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 배열을 복호화하는 데 사용되며, 디코딩되고 순차적으로 (당신이 그것을 디코딩 또는 중첩 용기 메소드를 호출 할 때마다, 그것은 상기 어레이에서 다음 요소로 진행 IE). 하나를 반복하는 방법은 답변의 두 번째 부분을 참조하십시오.

(최상위 수준 에 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)
            )
        }
    }
}

명확히해야 할 한 가지는 : 무엇을 의미 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 JSON 객체를 디코딩하는 데 필요한 모든 키 가있는 하나의 큰 CodingKeys열거 형을 갖는 대신 각 JSON 객체에 대해 여러 열거 형으로 분할해야 함을 의미합니다. 예를 들어 위 코드 에서 키와 함께 사용자 JSON 객체 ( ) 를 디코딩하기 위해 & . CodingKeys.User{ "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }"user_name""real_info"
해미

감사. 매우 명확한 응답. 나는 그것을 완전히 이해하기 위해 여전히 그것을 살펴보고 있습니다. 하지만 작동합니다.
FlowUI. SimpleUITesting.com

reviews_count사전 배열에 대해 한 가지 질문이있었습니다 . 현재 코드는 예상대로 작동합니다. 내 reviewsCount는 배열에 하나의 값만 있습니다. 하지만 실제로 review_count 배열을 원한다면 배열로 선언 var reviewCount: Int하면됩니까? -> var reviewCount: [Int]. 그런 다음 ReviewsCount열거 형도 편집해야 합니까?
FlowUI. SimpleUITesting.com

1
@JTAppleCalendarforiOSSwift 설명하는 것은 단순한 배열이 Int아니라 각각 Int주어진 키에 대한 값을 갖는 JSON 객체의 배열이기 때문에 실제로 약간 더 복잡 할 것입니다. 설정 해제 된 컨테이너와는 디코딩, 모든 중첩 된 키 입력 용기를 얻을 Int, 예를 들어 각각에 대해 (다음 배열에 사람들을 추가) gist.github.com/hamishknight/9b5c202fe6d8289ee2cb9403876a1b41
해미

4

많은 좋은 답변이 이미 게시되었지만 아직 설명되지 않은 더 간단한 방법이 있습니다 IMO.

JSON 필드 이름을 사용하여 작성 snake_case_notation하면 camelCaseNotationSwift 파일에서 계속 사용할 수 있습니다 .

설정 만하면됩니다.

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
이것은 다른 수준의 중첩을 처리하는 방법에 대한 원래의 질문을 다루지 않습니다.
Theo

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)
  }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.