Reputation: 1720
Let's say I have the following decodable struct as an example illustrating what I'm trying to do:
struct Object: Decodable {
var id: String
var name: String
}
and this JSON:
[
{
"id": "a",
"name": "test"
},
{
"id": "b",
"name": null
}
]
Notice that the name property can be null
sometimes. This would mostly work fine like it is since the json keys match the struct property names, so I don't need a CodingKey
enum, but the name
property can be null sometimes. However, instead of making name
optional, I want to substitute a default string, so I need a custom initializer:
struct Object: Decodable {
var id: String
var name: String
init(from decoder: Decoder) throws {
...
self.name = <value from decoder> ?? "default name"
...
}
}
But this requires a CodingKey
object. I'm using the default keys. Do I need a CodingKey
enum as well now? Even though all my keys match up? Or is there a way to have a custom Decodable
initializer using just the keys as they are?
Is there maybe some sort of default container I can use?
let container = try decoder.container(keyedBy: <defaultContainer???>)
I tried using both of these variants, but neither worked:
let container = try decoder.singleValueContainer()
let container = try decoder.unkeyedContainer()
How can I have a custom Decodable
intializer but also use the default keys?
Upvotes: 9
Views: 15348
Reputation: 130092
You can emulate the behavior using property wrappers but it's not the perfect solution and it's a bit hacky. The problem has been discussed on Swift forums multiple times already.
With a property wrapper:
struct Object: Decodable {
var id: String
@DecodableDefault var name: String
}
Code for the property wrapper:
public protocol DecodableDefaultValue: Decodable {
static var defaultDecodableValue: Self { get }
}
@propertyWrapper
public struct DecodableDefault<T: Decodable>: Decodable {
public var wrappedValue: T
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try container.decode(T.self)
}
public init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}
extension DecodableDefault: Encodable where T: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
extension DecodableDefault: Equatable where T: Equatable { }
extension DecodableDefault: Hashable where T: Hashable { }
public extension KeyedDecodingContainer {
func decode<T: DecodableDefaultValue>(_: DecodableDefault<T>.Type, forKey key: Key) throws -> DecodableDefault<T> {
guard let value = try decodeIfPresent(DecodableDefault<T>.self, forKey: key) else {
return DecodableDefault(wrappedValue: T.defaultDecodableValue)
}
return value
}
}
extension Array: DecodableDefaultValue where Element: Decodable {
public static var defaultDecodableValue: [Element] {
return []
}
}
extension Dictionary: DecodableDefaultValue where Key: Decodable, Value: Decodable {
public static var defaultDecodableValue: [Key: Value] {
return [:]
}
}
extension String: DecodableDefaultValue {
public static let defaultDecodableValue: String = ""
}
extension Int: DecodableDefaultValue {
public static let defaultDecodableValue: Int = 0
}
To list a few problems:
let
, you need a separate immutable wrapper.Upvotes: 2
Reputation: 437422
The issue is that CodingKeys
is only automatically generated for you if didn’t fully manually conform to the relevant protocols. (This is very familiar for Objective-C developers, where a property’s backing ivar would not be automatically synthesized if you manually implemented all the relevant accessor methods.)
So, in the following scenarios, the CodingKeys
is not created automatically for you:
You adopt only Decodable
and implemented your own init(from:)
;
You adopt only Encodable
and implemented your own encode(to:)
; or
You adopt both Encodable
and Decodable
(or just to Codable
) and implemented your own init(from:)
and encode(to:)
.
So your case falls within the first scenario, above.
It has been suggested that you can get around your conundrum by adopting Codable
, even though you’ve only implemented init(from:)
and presumably don’t plan on ever using the Encodable
behavior. In effect, that is relying upon a side effect of a protocol you don’t plan on really using.
It doesn’t really matter too much, and adopting Codable
works, but it might be considered to be “more correct” to go ahead and define your CodingKeys
, rather than relying on the unimplemented Encodable
side-effect:
struct Object: Decodable {
var id: String
var name: String
enum CodingKeys: String, CodingKey {
case id, name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = (try? container.decode(String.self, forKey: .name)) ?? "Default Value"
}
}
Upvotes: 12
Reputation:
The auto-generation for CodingKeys
is really weird. The scope and availability of it changes based on what members you have.
Say you just have a Decodable
. These compile:
struct Decodable: Swift.Decodable {
static var codingKeysType: CodingKeys.Type { CodingKeys.self }
}
struct Decodable: Swift.Decodable {
static func `init`(from decoder: Decoder) throws -> Self {
_ = CodingKeys.self
return self.init()
}
}
…and you can put them together, if you add private
.
struct Decodable: Swift.Decodable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
static func `init`(from decoder: Decoder) throws -> Self {
_ = CodingKeys.self
return self.init()
}
}
…But make that func
an initializer, and again, no compilation.
struct Decodable: Swift.Decodable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
init(from decoder: Decoder) throws {
_ = CodingKeys.self
}
}
You can change it to be fully Codable
, not just Decodable
…
struct Decodable: Codable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
init(from decoder: Decoder) throws {
_ = CodingKeys.self
}
}
But then you can't use CodingKeys
at type scope, so the property won't compile.
Considering you probably don't need such a property, just use Codable
, file a bug with Apple referencing this answer, and hopefully we can all switch to Decodable
when they fix it. 😺
Upvotes: 3
Reputation: 1529
As a comment made on your question says the compiler generates a CodingKeys
object for you. You can implement a custom enum
when the keys mismatch names on your enum or class respecting the JSON data you're receiving from the source.
You can implement your object this way:
struct TestObject: Codable {
var id: String
var name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(String.self, forKey: .id)
let nameOrNil = try? container.decode(String.self, forKey: .name)
self.id = id
self.name = nameOrNil ?? "Default value"
}
}
The container
's decode(_:forKey:)
method can throw and error but if you make the implementation to discharge the error and instead returning an optional value with try?
you can apply a nil coalescing operator to assign your default value whenever the name is not included on the JSON.
Proof here:
let json = """
[
{
"id": "a",
"name": "test"
},
{
"id": "b",
"name": null
}
]
""".data(using: .utf8)!
let decodedArray = try JSONDecoder().decode([TestObject].self, from: json)
print(decodedArray)
References:
Upvotes: 1