[json] Swift Decodable 프로토콜로 중첩 된 JSON 구조체를 디코딩하는 방법은 무엇입니까?
여기 내 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의 다양한 수준을 올바르게 수행하는 방법을 여전히 이해하지 못합니다. 어떤 도움이라도 대단히 감사하겠습니다.
답변
또 다른 접근 방식은 ( 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
나중에 둘 이상의 값을 포함 할 경우을 통해 쉽게 반복 할 수 있습니다 .
답변
문제를 해결하기 위해 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
*/
답변
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:)
다음 메서드를 반복적으로 사용할 수 있습니다.
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)
)
}
}
}
답변
많은 좋은 답변이 이미 게시되었지만 아직 설명되지 않은 더 간단한 방법이 있습니다 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)
}
답변
- json 파일을 https://app.quicktype.io에 복사합니다.
- Swift 선택 (Swift 5를 사용하는 경우 Swift 5의 호환성 스위치 확인)
- 다음 코드를 사용하여 파일 디코딩
- 짜잔!
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)
답변
또한 내가 준비한 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)
}
}