[json] Swift 4의 JSONDecoder를 사용하면 누락 된 키가 선택적 속성이 아닌 기본값을 사용할 수 있습니까?

Swift 4는 새로운 Codable프로토콜을 추가했습니다 . 사용할 때 JSONDecoderCodable클래스 의 모든 비 선택적 속성이 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)
    }
}

이 방법은 원하는대로 최종 개체의 이름을 바꾸고 변경할 수 있기 때문에 좋습니다. 명확하고 수동 디코딩보다 적은 코드가 필요합니다. 또한이 접근 방식을 사용하면 다른 앱에서 네트워킹 계층을 분리 할 수 ​​있습니다.


답변

init(from decoder: Decoder)기본 구현을 사용하는 대신 유형 에서 메소드를 구현할 수 있습니다 .

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
        }
    }
}

name원하는 경우 상수 속성을 만들 수도 있습니다 .

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"
        }
    }
}

또는

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"
}

댓글 다시 작성 : 사용자 지정 확장 사용

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}

init 메소드를 다음과 같이 구현할 수 있습니다.

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}

그러나 그것은 훨씬 짧지 않습니다

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"


답변

한 가지 해결책은 JSON 키를 찾을 수없는 경우 원하는 값으로 기본 설정되는 계산 된 속성을 사용하는 것입니다. 이렇게하면 다른 속성을 선언해야하므로 추가 세부 정보가 추가되고 CodingKeys열거 형을 추가해야합니다 (아직없는 경우). 장점은 사용자 지정 디코딩 / 인코딩 코드를 작성할 필요가 없다는 것입니다.

예를 들면 :

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
    }
}


답변

구현할 수 있습니다.

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)
    }
}


답변

인코딩 및 디코딩 방법을 구현하지 않으려면 기본값에 대한 다소 더러운 솔루션이 있습니다.

새 필드를 암시 적으로 언 래핑 된 옵션으로 선언하고 디코딩 후 nil인지 확인하고 기본값을 설정할 수 있습니다.

PropertyListEncoder로만 테스트했지만 JSONDecoder가 같은 방식으로 작동한다고 생각합니다.


답변

자신의 버전을 작성한다고 생각한다면 init(from decoder: Decoder) 것이 압도적 되면 입력을 디코더로 보내기 전에 입력을 확인하는 메서드를 구현하도록 조언합니다. 이렇게하면 필드가 없는지 확인하고 자신의 기본값을 설정할 수 있습니다.

예를 들면 :

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.
}

그리고 json에서 객체를 초기화하려면 다음 대신 :

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은 다음과 같습니다.

if let vuvVideoFile = PublicVideoFile.customDecode($0) {
    videos.append(vuvVideoFile)
}

이 특정 상황에서는 선택 사항을 처리하는 것을 선호하지만 다른 의견이 있으면 customDecode (:) 메서드를 throw 가능하게 만들 수 있습니다.


답변

나는 똑같은 것을 찾고있는이 질문을 보았습니다. 내가 찾은 답은 여기에있는 해결책이 유일한 선택 일 까봐 두려웠지만 그다지 만족스럽지 못했습니다.

제 경우에는 사용자 지정 디코더를 만들려면 유지 관리하기 어려운 수많은 상용구가 필요하므로 다른 답변을 계속 검색했습니다.

.NET을 사용하는 간단한 경우에 이것을 극복하는 흥미로운 방법을 보여주는 이 기사 를 만났습니다 @propertyWrapper. 저에게 가장 중요한 것은 재사용이 가능하고 기존 코드를 최소한으로 리팩토링해야한다는 것입니다.

이 기사에서는 누락 된 부울 속성이 실패하지 않고 기본적으로 false로 설정되기를 원하는 경우를 가정하고 다른 변형도 보여줍니다. 더 자세히 읽을 수 있지만 사용 사례를 위해 내가 한 일을 보여줄 것입니다.

제 경우 array에는 키가 없으면 비어있는 것으로 초기화하고 싶었습니다.

그래서 다음 @propertyWrapper과 같은 추가 확장을 선언했습니다 .

@propertyWrapper
struct DefaultEmptyArray<T:Codable> {
    var wrappedValue: [T] = []
}

//codable extension to encode/decode the wrapped value
extension DefaultEmptyArray: Codable {

    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = try container.decode([T].self)
    }

}

extension KeyedDecodingContainer {
    func decode<T:Decodable>(_ type: DefaultEmptyArray<T>.Type,
                forKey key: Key) throws -> DefaultEmptyArray<T> {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

이 방법의 장점은 단순히 @propertyWrapper속성 에를 추가하여 기존 코드의 문제를 쉽게 극복 할 수 있다는 것 입니다. 나의 경우에는:

@DefaultEmptyArray var items: [String] = []

이것이 같은 문제를 다루는 사람에게 도움이되기를 바랍니다.


최신 정보:

문제를 계속 조사 하면서이 답변을 게시 한 후이 다른 기사를 찾았 지만 가장 중요한 것은 @propertyWrapper이러한 종류의 경우에 일반적으로 사용하기 쉬운을 포함하는 각 라이브러리입니다 .

https://github.com/marksands/BetterCodable