Com JSONDecoder no Swift 4, as chaves ausentes podem usar um valor padrão em vez de serem propriedades opcionais?

114

Swift 4 adicionou o novo Codableprotocolo. Quando eu uso JSONDecoder, parece exigir que todas as propriedades não opcionais da minha Codableclasse tenham chaves no JSON ou ele gera um erro.

Tornar todas as propriedades da minha classe opcionais parece um aborrecimento desnecessário, pois o que eu realmente quero é usar o valor no json ou um valor padrão. (Não quero que a propriedade seja nula.)

Existe uma maneira de fazer isso?

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
fonte
Mais uma consulta, o que posso fazer se eu tiver várias chaves em meu json e quiser escrever um método genérico para mapear json para criar objeto em vez de dar nulo, deve fornecer pelo menos o valor padrão.
Aditya Sharma

Respostas:

22

A abordagem que eu prefiro é usar os chamados DTOs - objeto de transferência de dados. É uma estrutura que está em conformidade com Codable e representa o objeto desejado.

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

Em seguida, você simplesmente inicia o objeto que deseja usar no aplicativo com esse 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)
    }
}

Essa abordagem também é boa, pois você pode renomear e alterar o objeto final como desejar. É claro e requer menos código do que a decodificação manual. Além disso, com essa abordagem, você pode separar a camada de rede de outro aplicativo.

Leonid Silver
fonte
Algumas das outras abordagens funcionaram bem, mas, no final das contas, acho que algo nesse sentido é a melhor abordagem.
zekel
bom para saber, mas há muita duplicação de código. Eu prefiro a resposta de Martin R
Kamen Dobrev
136

Você pode implementar o init(from decoder: Decoder)método em seu tipo em vez de usar a implementação padrão:

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

Você também pode fazer nameuma propriedade constante (se quiser):

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

ou

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 seu comentário: Com uma extensão personalizada

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

você pode implementar o método init como

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

mas isso não é muito mais curto do que

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
Martin R
fonte
Observe também que, neste caso específico, você pode usar a CodingKeysenumeração gerada automaticamente (para remover a definição personalizada) :)
Hamish
@Hamish: Não compilou quando tentei pela primeira vez, mas agora funciona :)
Martin R
Sim, atualmente está um pouco irregular, mas será corrigido ( bugs.swift.org/browse/SR-5215 )
Hamish
54
Ainda é ridículo que os métodos gerados automaticamente não possam ler os valores padrão de não opcionais. Eu tenho 8 opcionais e 1 não opcional, portanto, escrever manualmente os métodos do codificador e do decodificador traria muitos clichês. ObjectMapperlida com isso muito bem.
Legoless
1
@LeoDabus Será que você está se adaptando Decodablee também fornecendo sua própria implementação init(from:)? Nesse caso, o compilador assume que você deseja manipular a decodificação manualmente e, portanto, não sintetiza um CodingKeysenum para você. Como você disse, conformar-se com Codablefunciona porque agora o compilador está sintetizando encode(to:)para você e, portanto, também sintetiza CodingKeys. Se você também fornecer sua própria implementação de encode(to:), CodingKeysnão será mais sintetizado.
Hamish de
37

Uma solução seria usar uma propriedade computada cujo padrão é o valor desejado se a chave JSON não for encontrada. Isso adiciona um pouco de verbosidade extra, pois você precisará declarar outra propriedade e exigirá a adição do CodingKeysenum (se ainda não estiver lá). A vantagem é que você não precisa escrever um código de decodificação / codificação personalizado.

Por exemplo:

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
    }
}
Cristik
fonte
Abordagem interessante. Ele adiciona um pouco de código, mas é muito claro e inspecionável depois que o objeto é criado.
zekel
Minha resposta favorita para esta questão. Ele me permite ainda usar o JSONDecoder padrão e facilmente fazer uma exceção para uma variável. Obrigado.
iOS_Mouse
Nota: Usando essa abordagem, sua propriedade torna-se apenas get, você não pode atribuir valor diretamente a esta propriedade.
Ganpat
8

Você pode implementar.

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)
    }
}
Ankit
fonte
sim, esta é a resposta mais limpa, mas ainda obtém muito código quando você tem objetos grandes!
Ashkan Ghodrat
1

Se você não deseja implementar seus métodos de codificação e decodificação, existe uma solução um tanto suja em torno dos valores padrão.

Você pode declarar seu novo campo como opcional desempacotado implicitamente e verificar se ele é nulo após a decodificação e definir um valor padrão.

Eu testei isso apenas com PropertyListEncoder, mas acho que JSONDecoder funciona da mesma maneira.

Kirill Kuzyk
fonte
1

Me deparei com essa pergunta procurando exatamente a mesma coisa. As respostas que encontrei não foram muito satisfatórias, embora eu tivesse medo de que as soluções aqui fossem a única opção.

No meu caso, a criação de um decodificador customizado exigiria uma tonelada de clichês que seria difícil de manter, então continuei procurando outras respostas.

Encontrei este artigo que mostra uma maneira interessante de superar isso em casos simples usando um @propertyWrapper. O mais importante para mim era que ele era reutilizável e exigia um mínimo de refatoração do código existente.

O artigo presume um caso em que você deseja que uma propriedade booleana ausente seja padronizada como false sem falhar, mas também mostra outras variantes diferentes. Você pode ler com mais detalhes, mas mostrarei o que fiz para o meu caso de uso.

No meu caso, eu tinha um arrayque queria inicializar como vazio se a chave estivesse faltando.

Portanto, declarei as seguintes @propertyWrappere outras extensões:

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

A vantagem desse método é que você pode facilmente superar o problema no código existente simplesmente adicionando o @propertyWrapperà propriedade. No meu caso:

@DefaultEmptyArray var items: [String] = []

Espero que isso ajude alguém a lidar com o mesmo problema.


ATUALIZAR:

Depois de postar esta resposta e continuar a investigar o assunto, encontrei este outro artigo, mas o mais importante, a respectiva biblioteca que contém alguns s comuns fáceis de usar @propertyWrapperpara esses tipos de casos:

https://github.com/marksands/BetterCodable

Barbosa
fonte
0

Se você acha que escrever sua própria versão do init(from decoder: Decoder)é opressor, recomendo que implemente um método que verificará a entrada antes de enviá-la para o decodificador. Dessa forma, você terá um local onde poderá verificar a ausência de campos e definir seus próprios valores padrão.

Por exemplo:

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

E para iniciar um objeto de json, em vez de:

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 será parecido com este:

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

Nesta situação particular, eu prefiro lidar com opcionais, mas se você tiver uma opinião diferente, você pode tornar seu método customDecode (:) jogável

Eugene Alexeev
fonte