Swift 4의 JSONDecoder를 사용하면 누락 된 키가 선택적 속성이 아닌 기본값을 사용할 수 있습니까?
Swift 4는 새로운 Codable
프로토콜을 추가했습니다 . 사용할 때 JSONDecoder
내 Codable
클래스 의 모든 비 선택적 속성이 JSON에 키를 갖도록 요구하거나 오류가 발생합니다.
내 클래스의 모든 속성을 선택적으로 만드는 것은 json의 값이나 기본값을 사용하는 것이기 때문에 불필요한 번거 로움처럼 보입니다. (저는 속성이 0이기를 원하지 않습니다.)
이 작업을 수행하는 방법이 있습니까?
class MyCodable: Codable {
var name: String = "Default Appleseed"
}
func load(input: String) {
do {
if let data = input.data(using: .utf8) {
let result = try JSONDecoder().decode(MyCodable.self, from: data)
print("name: \(result.name)")
}
} catch {
print("error: \(error)")
// `Error message: "Key not found when expecting non-optional type
// String for coding key \"name\""`
}
}
let goodInput = "{\"name\": \"Jonny Appleseed\" }"
let badInput = "{}"
load(input: goodInput) // works, `name` is Jonny Applessed
load(input: badInput) // breaks, `name` required since property is non-optional
내가 선호하는 접근 방식은 소위 DTO (데이터 전송 개체)를 사용하는 것입니다. Codable을 준수하고 원하는 객체를 나타내는 구조체입니다.
struct MyClassDTO: Codable {
let items: [String]?
let otherVar: Int?
}
그런 다음 해당 DTO로 앱에서 사용하려는 개체를 초기화하면됩니다.
class MyClass {
let items: [String]
var otherVar = 3
init(_ dto: MyClassDTO) {
items = dto.items ?? [String]()
otherVar = dto.otherVar ?? 3
}
var dto: MyClassDTO {
return MyClassDTO(items: items, otherVar: otherVar)
}
}
This approach is also good since you can rename and change final object however you wish to. It is clear and requires less code than manual decoding. Moreover, with this approach you can separate networking layer from other app.
You can implement the init(from decoder: Decoder)
method in your type instead of using the default implementation:
class MyCodable: Codable {
var name: String = "Default Appleseed"
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let name = try container.decodeIfPresent(String.self, forKey: .name) {
self.name = name
}
}
}
You can also make name
a constant property (if you want to):
class MyCodable: Codable {
let name: String
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let name = try container.decodeIfPresent(String.self, forKey: .name) {
self.name = name
} else {
self.name = "Default Appleseed"
}
}
}
or
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
}
Re your comment: With a custom extension
extension KeyedDecodingContainer {
func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
where T : Decodable {
return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
}
}
you could implement the init method as
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}
but that is not much shorter than
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
One solution would be to use a computed property that defaults to the desired value if the JSON key is not found. This adds some extra verbosity as you'll need to declare another property, and will require adding the CodingKeys
enum (if not already there). The advantage is that you don't need to write custom decoding/encoding code.
For example:
class MyCodable: Codable {
var name: String { return _name ?? "Default Appleseed" }
var age: Int?
private var _name: String?
enum CodingKeys: String, CodingKey {
case _name = "name"
case age
}
}
If you don't want to implement your encoding and decoding methods, there is somewhat dirty solution around default values.
You can declare your new field as implicitly unwrapped optional and check if it's nil after decoding and set a default value.
I tested this only with PropertyListEncoder, but I think JSONDecoder works the same way.
You can implement.
struct Source : Codable {
let id : String?
let name : String?
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
If you think that writing your own version of init(from decoder: Decoder)
is overwhelming, I would advice you to implement a method which will check the input before sending it to decoder. That way you'll have a place where you can check for fields absence and set your own default values.
For example:
final class CodableModel: Codable
{
static func customDecode(_ obj: [String: Any]) -> CodableModel?
{
var validatedDict = obj
let someField = validatedDict[CodingKeys.someField.stringValue] ?? false
validatedDict[CodingKeys.someField.stringValue] = someField
guard
let data = try? JSONSerialization.data(withJSONObject: validatedDict, options: .prettyPrinted),
let model = try? CodableModel.decoder.decode(CodableModel.self, from: data) else {
return nil
}
return model
}
//your coding keys, properties, etc.
}
And in order to init an object from json, instead of:
do {
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
let model = try CodableModel.decoder.decode(CodableModel.self, from: data)
} catch {
assertionFailure(error.localizedDescription)
}
Init will look like this:
if let vuvVideoFile = PublicVideoFile.customDecode($0) {
videos.append(vuvVideoFile)
}
In this particular situation I prefer to deal with optionals but if you have a different opinion, you can make your customDecode(:) method throwable
'code' 카테고리의 다른 글
Visual Studio Code가 설치된 git을 감지 할 수 없음 (0) | 2020.09.23 |
---|---|
npm -D 플래그는 무엇을 의미합니까? (0) | 2020.09.22 |
std :: set에 대한 std :: back_inserter? (0) | 2020.09.22 |
정의되지 않은 것을 덮어 쓰지 않는다고 가정하는 것이 JavaScript에서 실제로 얼마나 위험한가요? (0) | 2020.09.22 |
IIS 앱 풀, 작업자 프로세스, 앱 도메인 (0) | 2020.09.22 |