可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Say I have the following code:
import Foundation
let jsonData = """
[
{"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
{"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!
struct Person: Codable {
let firstName, lastName: String
let age: String?
enum CodingKeys : String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case age
}
}
let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded)
Everything is working except age
is always nil
. Which makes sense. My question is how can I set the Person's age = realage
or 28
in the first example, and nil
in the second example. Instead of age
being nil
in both cases I want it to be 28
in the first case.
Is there a way to achieve this only using CodingKeys
and not having to add another struct or class? If not how can I use another struct or class to achieve what I want in the simplest way possible?
回答1:
My favorite approach when it comes to decoding nested JSON data is to define a "raw" model that stays very close to the JSON, even using snake_case
if needed. It help bringing JSON data into Swift really quickly, then you can use Swift to do the manipulations you need:
struct Person: Decodable {
let firstName, lastName: String
let age: String?
// This matches the keys in the JSON so we don't have to write custom CodingKeys
private struct RawPerson: Decodable {
struct RawAge: Decodable {
let realage: String?
let fakeage: String?
}
let firstname: String
let lastname: String
let age: RawAge
}
init(from decoder: Decoder) throws {
let rawPerson = try RawPerson(from: decoder)
self.firstName = rawPerson.firstname
self.lastName = rawPerson.lastname
self.age = rawPerson.age.realage
}
}
Also, I recommend you to be judicious with the use of Codable
, as it implies both Encodable
and Decodable
. It seems like you only need Decodable
so conform your model to that protocol only.
回答2:
For greater flexibility and robustness, you could implement an Age
enumeration to fully support your data model head-on ;) For instance:
enum Age: Decodable {
case realAge(String)
case fakeAge(String)
private enum CodingKeys: String, CodingKey {
case realAge = "realage", fakeAge = "fakeage"
}
init(from decoder: Decoder) throws {
let dict = try decoder.container(keyedBy: CodingKeys.self)
if let age = try dict.decodeIfPresent(String.self, forKey: .realAge) {
self = .realAge(age)
return
}
if let age = try dict.decodeIfPresent(String.self, forKey: .fakeAge) {
self = .fakeAge(age)
return
}
let errorContext = DecodingError.Context(
codingPath: dict.codingPath,
debugDescription: "Age decoding failed"
)
throw DecodingError.keyNotFound(CodingKeys.realAge, errorContext)
}
}
and then use it in your Person
type:
struct Person: Decodable {
let firstName, lastName: String
let age: Age
enum CodingKeys: String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case age
}
var realAge: String? {
switch age {
case .realAge(let age): return age
case .fakeAge: return nil
}
}
}
Decode as before:
let jsonData = """
[
{"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
{"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!
let decoded = try! JSONDecoder().decode([Person].self, from: jsonData)
for person in decoded { print(person) }
prints:
Person(firstName: "Tom", lastName: "Smith", age: Age.realAge("28"))
Person(firstName: "Bob", lastName: "Smith", age: Age.fakeAge("31"))
Finally, the new realAge
computed property provides the behavior you were after initially (i.e., non-nil only for real ages):
for person in decoded { print(person.firstName, person.realAge) }
Tom Optional("28")
Bob nil
回答3:
There are times to trick the API to get the interface you want.
let jsonData = """
[
{"firstname": "Tom", "lastname": "Smith", "age": {"realage": "28"}},
{"firstname": "Bob", "lastname": "Smith", "age": {"fakeage": "31"}}
]
""".data(using: .utf8)!
struct Person: Codable {
let firstName: String
let lastName: String
var age: String? { return _age["realage"] }
enum CodingKeys: String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case _age = "age"
}
private let _age: [String: String]
}
do {
let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded)
let encoded = try JSONEncoder().encode(decoded)
if let encoded = String(data: encoded, encoding: .utf8) { print(encoded) }
} catch {
print(error)
}
Here the API (firstName
, lastName
, age
) is kept and the JSON is preserved in both directions.
回答4:
You can use like this :
struct Person: Decodable {
let firstName, lastName: String
var age: Age?
enum CodingKeys: String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case age
}
}
struct Age: Decodable {
let realage: String?
}
You can call like this :
do {
let decoded = try JSONDecoder().decode([Person].self, from: jsonData)
print(decoded[0].age?.realage) // Optional("28")
print(decoded[1].age?.realage) // nil
} catch {
print("error")
}
回答5:
Lots of great answers here. I have certain reasons for not wanting to make it into it's own data model. Specially in my case it comes with a lot of data I don't need and this specific thing I need corresponds more to a person than an age model.
I'm sure others will find this post useful tho which is amazing. Just to add to that I will post my solution for how I decided to do this.
After looking at the Encoding and Decoding Custom Types Apple Documentation, I found it was possible to build a custom decoder and encoder to achieve this (Encode and Decode Manually).
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
var elevation: Double
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case additionalInfo
}
enum AdditionalInfoKeys: String, CodingKey {
case elevation
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
latitude = try values.decode(Double.self, forKey: .latitude)
longitude = try values.decode(Double.self, forKey: .longitude)
let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
try additionalInfo.encode(elevation, forKey: .elevation)
}
}
The one change that is included in the code above that Apple doesn't mention is the fact that you can't use extensions like in their documentation example. So you have to embed it right within the struct or class.
Hopefully this helps someone, along with the other amazing answers here.