gnarlybracket
gnarlybracket

Reputation: 1720

Swift custom decodable initializer without CodingKeys

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

Answers (4)

Sulthan
Sulthan

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:

  • you cannot select the default value (can be done differently but it's more complicated)
  • if you want to use let, you need a separate immutable wrapper.

Upvotes: 2

Rob
Rob

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:

  1. You adopt only Decodable and implemented your own init(from:);

  2. You adopt only Encodable and implemented your own encode(to:); or

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

user652038
user652038

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

Orlando
Orlando

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

Related Questions