parser jsonserialization json swift swift4 codable

jsonserialization - Con JSONDecoder en Swift 4, ¿pueden las teclas faltantes usar un valor predeterminado en lugar de tener que ser propiedades opcionales?



swift codable json (6)

Swift 4 agregó el nuevo protocolo Codeable . Cuando uso JSONDecoder parece requerir que todas las propiedades no opcionales de mi clase Codeable tengan claves en el JSON o arroja un error.

Hacer que cada propiedad de mi clase sea opcional parece una molestia innecesaria ya que lo que realmente quiero es usar el valor en el json o un valor predeterminado. (No quiero que la propiedad sea nula).

¿Hay alguna forma de hacer esto?

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


El enfoque que prefiero es usar los llamados DTO: objeto de transferencia de datos. Es una estructura, que se ajusta a Codificable y representa el objeto deseado.

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

Luego, simplemente inicie el objeto que desea usar en la aplicación con ese DTO.

do { let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) let model = try CodableModel.decoder.decode(CodableModel.self, from: data) } catch { assertionFailure(error.localizedDescription) }

Este enfoque también es bueno ya que puede cambiar el nombre y cambiar el objeto final como lo desee. Es claro y requiere menos código que la decodificación manual. Además, con este enfoque, puede separar la capa de red de otra aplicación.


Otra solución sería utilizar una propiedad calculada que tenga como valor predeterminado el valor deseado si no se encuentra la clave JSON. Esto también agrega un poco de verbosidad adicional, ya que deberá declarar otra propiedad, y requerirá agregar una enumeración CodingKeys (si aún no está allí). La ventaja es que no necesita escribir código de decodificación / codificación personalizado.

Por ejemplo:

class MyCodable: Codable { var name: String { return _name ?? "Default Appleseed" } private var _name: String? enum CodingKeys: String, CodingKey { case _name = "name" } }


Puede implementar el método init(from decoder: Decoder) en su tipo en lugar de utilizar la implementación predeterminada:

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

También puede hacer que el name una propiedad constante (si lo desea):

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

o

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 su comentario: Con una extensión personalizada

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

podrías implementar el 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") }

pero eso no es mucho más corto que

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


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


Si cree que escribir su propia versión de init(from decoder: Decoder) es abrumador, le aconsejaría que implemente un método que verifique la entrada antes de enviarla al decoder. De esa manera, tendrá un lugar donde puede verificar la ausencia de campos y establecer sus propios valores predeterminados.

Por ejemplo:

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

Y para iniciar un objeto desde json, en lugar 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 se verá así:

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

En esta situación particular, prefiero tratar con opciones, pero si tiene una opinión diferente, puede hacer que su método customDecode (:) sea arrojable


Si no desea implementar sus métodos de codificación y decodificación, hay una solución algo sucia en torno a los valores predeterminados.

Puede declarar su nuevo campo como opcional sin envolver implícitamente y verificar si es nulo después de la decodificación y establecer un valor predeterminado.

Probé esto solo con PropertyListEncoder, pero creo que JSONDecoder funciona de la misma manera.