Swift 4 добавил новый протокол Codable. Когда я использую JSONDecoder, кажется, что все необязательные свойства моего класса Codable должны иметь ключи в JSON, иначе возникает ошибка.

Делать каждое свойство моего класса необязательным кажется ненужной проблемой, поскольку я действительно хочу использовать значение в json или значение по умолчанию. (Я не хочу, чтобы свойство было равно нулю.)

Есть способ сделать это?

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

zekel

Ответов: 7

Ответы (7)

Подход, который я предпочитаю, заключается в использовании так называемых DTO - объекта передачи данных. Это структура, соответствующая Codable и представляющая желаемый объект.

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}

Then you simply init the object that you want to use in the app with that 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.

Вы можете реализовать метод init (из 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
        }
    }
}

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

Можно реализовать.

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

Если вы думаете, что написание собственной версии init (из decoder: Decoder) - это непосильная задача, я бы посоветовал вам реализовать метод, который будет проверять ввод перед отправкой его в декодер. Таким образом, у вас будет место, где вы можете проверить отсутствие полей и установить свои собственные значения по умолчанию.

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

Я наткнулся на этот вопрос в поисках того же самого. Ответы, которые я нашел, не очень удовлетворительны, хотя я боялся, что решения здесь будут единственным вариантом.

In my case, creating a custom decoder would require a ton of boilerplate that would be hard to maintain so I kept searching for other answers.

I ran into this article that shows an interesting way to overcome this in simple cases using a @propertyWrapper. The most important thing for me, was that it was reusable and required minimal refactoring of existing code.

The article assumes a case where you'd want a missing boolean property to default to false without failing but also shows other different variants. You can read it in more detail but I'll show what I did for my use case.

In my case, I had an array that I wanted to be initialized as empty if the key was missing.

So, I declared the following @propertyWrapper and additional extensions:

@propertyWrapper
struct DefaultEmptyArray {
    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(_ type: DefaultEmptyArray.Type,
                forKey key: Key) throws -> DefaultEmptyArray {
        try decodeIfPresent(type, forKey: key) ?? .init()
    }
}

The advantage of this method is that you can easily overcome the issue in existing code by simply adding the @propertyWrapper to the property. In my case:

@DefaultEmptyArray var items: [String] = []

Hope this helps someone dealing with the same issue.


UPDATE:

After posting this answer while continuing to look into the matter I found this other article but most importantly the respective library that contains some common easy to use @propertyWrappers for these kind of cases:

https://github.com/marksands/BetterCodable

Если вы не хотите реализовывать свои методы кодирования и декодирования, есть несколько грязное решение для значений по умолчанию.

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.

Вы можете использовать вычисляемое свойство, которое по умолчанию принимает желаемое значение, если ключ JSON не найден.

class MyCodable: Codable {
    var name: String { return _name ?? "Default Appleseed" }
    var age: Int?

    // this is the property that gets actually decoded/encoded
    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
        case age
    }
}

If you want to have the property readwrite, you can also implement the setter:

var name: String {
    get { _name ?? "Default Appleseed" }
    set { _name = newValue }
}

This adds a little 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, which can become tedious at some point.

Note that this solution only works if the value for the JSON key either holds a string, or is not present. If the JSON might have the value under another form (e.g. its an int), then you can try this solution.

2022 WebDevInsider