Codable conformance for nested Enum? - swift

I'm a bit tangled up on using Codable with nested enums. Suppose I have the following enum:
enum ItemStatus {
enum Foo {
case a
case b(Double, Double)
case c
}
enum Bar {
case x(String)
case y
case z(Int)
}
}
extension ItemStatus: Codable {
init(from decoder: Decoder) throws {
}
func encode(to encoder: Encoder) throws {
}
}
For normal enums, I'm implementing Codable like this. It works, but I'm not sure how to implement Codable for the top-level enum (in this case, ItemStats), and my gut tells me there is a more efficient way to write this anyway:
extension ItemStatus.Bar {
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "x": self = .x("")
case "y": self = .y
case "z": self = .z(0)
default: return nil
}
}
private var typeStr: String {
guard let label = Mirror(reflecting: self).children.first?.label else {
return .init(describing: self)
}
return label
}
}
extension ItemStatus.Bar : Codable {
private enum CodingKeys: String, CodingKey {
case type
case xVal
case zVal
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let type = try? container.decode(String.self, forKey: .type) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Could not get type of Bar."))
}
guard let base = ItemStatus.Bar(rawValue: type) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Could not init Bar with value: \(type)"))
}
switch base {
case .x:
let val = try container.decode(String.self, forKey: .xVal)
self = .x(val)
case .z:
let val = try container.decode(Int.self, forKey: .zVal)
self = .z(val)
default:
self = base
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.typeStr, forKey: .type)
switch self {
case .x(let val):
try container.encode(val, forKey: .xVal)
case .z(let val):
try container.encode(val, forKey: .zVal)
default: ()
}
}
}

Related

Firestore. Swift. Create document with dynamic field

I am new to Firestore. For example I have an Object like this:
struct Item: Codable {
let key: String?
let value: ItemAnyValueType?
}
public indirect enum ItemAnyValueType: Codable {
case string(String)
case bool(Bool)
public init(from decoder: Decoder) throws {
let singleValueContainer = try decoder.singleValueContainer()
if let value = try? singleValueContainer.decode(Bool.self) {
self = .bool(value)
return
} else if let value = try? singleValueContainer.decode(String.self) {
self = .string(value)
return
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Error"))
}
private enum CodingKeys: String, CodingKey {
case string
case bool
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .string(let value):
try container.encode(value, forKey: .string)
case .bool(let value):
try container.encode(value, forKey: .bool)
}
}
}
The item in Firestore looks like this:
documents
id: 123123 -> key: "test1", value: true
id: 323412 -> key: "test2", value: "good"
id: xx2332 -> key: "test3", value: "i love cats"
id: 433xxx -> key: "test4", value: "i love dogs"
The field value can be boolean or string.
Example of the saved data:
...
...
...
.document("433xxx")
.setData(from: Item(key: "433xxx", value: .string("i love orange")),
merge: true,
completion: { (error) in
print("ok")
})
but the code saves like this:
id: 433xxx -> key: "433xxx", value: map {string: "i love orange"}
Just like you did in your decoder section, you can use a singleValueContainer:
public indirect enum ItemAnyValueType: Codable {
case string(String)
case bool(Bool)
public init(from decoder: Decoder) throws {
let singleValueContainer = try decoder.singleValueContainer()
if let value = try? singleValueContainer.decode(Bool.self) {
self = .bool(value)
return
} else if let value = try? singleValueContainer.decode(String.self) {
self = .string(value)
return
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Error"))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let value):
try container.encode(value)
case .bool(let value):
try container.encode(value)
}
}
}

Polymorphically decode a sub-object of a KeyedDecodingContainer in Swift

The following code attempts to create a Codable type AnyBase that would allow you to encode and decode various subclasses of Base. As written, in fails, because it tries to lookup the type objects by a string code, using getTypeFromCode. But if you use the commented-out part instead with hard-coded types, it prints "Sub1", as desired.
Is there a way to adjust this to use something like getTypeFromCode and eliminate the hard-coded types from init?
class Base: Codable {
var foo: Int = 1
var codingTypeKey: String { fatalError("abstract") }
}
class Sub1: Base {
var sub1Prop: Int = 2
override var codingTypeKey: String { "sub1" }
enum CodingKeys: CodingKey {
case sub1Prop
}
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
sub1Prop = try c.decode(Int.self, forKey: .sub1Prop)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(sub1Prop, forKey: .sub1Prop)
try super.encode(to: encoder)
}
}
class Sub2: Base {
var sub2Prop: Int = 2
override var codingTypeKey: String { "sub2" }
}
func getTypeFromCode(_ typeCode: String) -> Base.Type {
if typeCode == "sub1" { return Sub1.self }
else if typeCode == "sub2" { return Sub2.self }
else { fatalError("bad type code") }
}
class AnyBase: Codable {
let base: Base
init(_ b: Base) { base = b }
required init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
let typeCode = try c.decode(String.self, forKey: .type)
/*if typeCode == "sub1" {
self.base = try c.decode(Sub1.self, forKey: .base)
} else if typeCode == "sub2" {
self.base = try c.decode(Sub2.self, forKey: .base)
} else {
fatalError("bad type code")
}*/
let typeObj = getTypeFromCode(typeCode)
self.base = try c.decode(typeObj, forKey: .base)
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(base.codingTypeKey, forKey: .type)
try c.encode(base, forKey: .base)
}
enum CodingKeys: CodingKey {
case base, type
}
}
// To to round-trip encode/decode and get the right object back...
let enc = JSONEncoder()
let dec = JSONDecoder()
let sub = Sub1()
let any = AnyBase(sub)
let data = try! enc.encode(any)
let str = String(data:data, encoding:.utf8)!
print(str)
let any2 = try! dec.decode(AnyBase.self, from: data)
print(type(of:any2.base))
Instead of getTypeFromCode (_:) method, you can create an enum BaseType like,
enum BaseType: String {
case sub1, sub2
var type: Base.Type {
switch self {
case .sub1: return Sub1.self
case .sub2: return Sub2.self
}
}
}
Now, in init(from:) get the BaseType using typeCode as rawValue, i.e.
class AnyBase: Codable {
required init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
let typeCode = try c.decode(String.self, forKey: .type)
if let baseType = BaseType(rawValue: typeCode) {
self.base = try c.decode(baseType.type, forKey: .base)
} else {
fatalError("bad type code")
}
}
//rest of the code...
}

Swift Codable how to use Any type?

When I try to access the value of "value" for example to use it in a label.text, I get an error
Cannot assign value of type 'MyValue?' to type 'String?'
When I print the value to the terminal, it says ...
unknown context at 0x109d06188).MyValue.string...
How can solve this problem?
struct Root: Codable {
let description,id: String
let group,groupDescription: String?
let name: String
let value: MyValue
enum CodingKeys: String, CodingKey {
case description = "Description"
case group = "Group"
case groupDescription = "GroupDescription"
case id = "Id"
case name = "Name"
case value = "Value"
}
}
enum MyValue: Codable {
case string(String)
case innerItem(InnerItem)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(InnerItem.self) {
self = .innerItem(x)
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .innerItem(let x):
try container.encode(x)
}
}
}
You can get string values for your label by conforming to rawRepresentable protocol:
enum MyValue: Codable, RawRepresentable {
var rawValue: String {
switch self {
case .string(let stringVal):
return stringVal
case .innerItem(let myVal):
return String(describing: myVal)
}
}
typealias RawValue = String
init?(rawValue: String) {
return nil
}
case string(String)
case innerItem(InnerItem)
}
let myVal = MyValue.string("testString")
var strVal: String = myVal.rawValue // testString
To get the associated values in the enum you could add two computed properties in MyValue
var stringValue : String? {
guard case .string(let string) = self else { return nil }
return string
}
var innerItemValue : InnerItem? {
guard case .innerItem(let innerItem) = self else { return nil }
return innerItem
}
Or switch on value like in the encode method
switch root.value {
case .string(let string): // do something with `string`
case .innerItem(let innerItem): // do something with `innerItem`
}
Or simply use if case
if case .string(let string) = root.value { someLabel.text = string }

Codable enum with multiple keys and associated values

I've seen answers on how to make an enum conform to Codable when all cases have an associated value, but I'm not clear on how to mix enums that have cases with and without associated values:
??? How can I use multiple variations of the same key for a given case?
??? How do I encode/decode cases with no associated value?
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "aaa", "a":
self = .aaa
case "bbb":
self = .bbb
case "ccc":
self = .ccc(0)
default: return nil
}
}
// Codable
private enum CodingKeys: String, CodingKey {
case aaa // ??? how can I accept "aaa", "AAA", and "a"?
case bbb
case ccc
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(Int.self, forKey: .ccc) {
self = .ccc(value)
return
}
// ???
// How do I decode the cases with no associated value?
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .ccc(let year):
try container.encode(year, forKey: .ccc)
default:
// ???
// How do I encode cases with no associated value?
}
}
}
Use the assumed raw string values of the init method as (string) value of the enum case
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "aaa", "a":
self = .aaa
case "bbb":
self = .bbb
case "ccc":
self = .ccc(0)
default: return nil
}
}
// Codable
private enum CodingKeys: String, CodingKey { case aaa, bbb, ccc }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(Int.self, forKey: .ccc) {
self = .ccc(value)
} else if let aaaValue = try? container.decode(String.self, forKey: .aaa), ["aaa", "AAA", "a"].contains(aaaValue) {
self = .aaa
} else if let bbbValue = try? container.decode(String.self, forKey: .bbb), bbbValue == "bbb" {
self = .bbb
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Data doesn't match"))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .aaa: try container.encode("aaa", forKey: .aaa)
case .bbb: try container.encode("bbb", forKey: .bbb)
case .ccc(let year): try container.encode(year, forKey: .ccc)
}
}
}
The Decoding Error is quite generic. You can throw more specific errors for each CodingKey
Starting with Swift 5.5, enums with associated values gained the ability of having automatic conformance to Codable. See this swift-evolution proposal for more details about the implementation details.
So, this is enough for your enum:
enum EmployeeClassification : Codable, Equatable {
case aaa
case bbb
case ccc(Int) // (year)
No more CodingKeys, no more init(from:), or encode(to:)
Thanks #vadian for the great answer 🙏🏻
Another way how to implement custom Decodable / Encodable methods for any enum with associated value and (or) empty cases – using approach with the nestedContainer method called from the root container.
That way describes in the Swift Evolution proposal for Swift 5.5 the support for auto-synthesis of Codable conformance to enums with associated values.
All details and the next implementation I got from the proposal you can also take a look: https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
I also extend example from the proposal to cover exactly author's question.
enum Command: Codable {
case load(String)
case store(key: String, Int)
case eraseAll
}
The encode(to:) implementation for Encodable would look as follows:
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .load(key):
var nestedContainer = container.nestedUnkeyedContainer(forKey: .load)
try nestedContainer.encode(key)
case let .store(key, value):
var nestedContainer = container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
try nestedContainer.encode(key, forKey: .key)
try nestedContainer.encode(value, forKey: .value)
case .eraseAll:
var nestedContainer = container.nestedUnkeyedContainer(forKey: .eraseAll)
try nestedContainer.encodeNil()
}
}
Please pay attention on some modifications: (1) for load and eraseAll cases I use nestedUnkeyedContainer instead suggested in proposal and eraseAll is the new case which I also added without an associated value.
and the init(from:) implementation for Decodable would look as follows:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.allKeys.count != 1 {
let context = DecodingError.Context(
codingPath: container.codingPath,
debugDescription: "Invalid number of keys found, expected one.")
throw DecodingError.typeMismatch(Command.self, context)
}
switch container.allKeys.first.unsafelyUnwrapped {
case .load:
let nestedContainer = try container.nestedUnkeyedContainer(forKey: .load)
self = .load(try nestedContainer.decode(String.self))
case .store:
let nestedContainer = try container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
self = .store(
key: try nestedContainer.decode(String.self, forKey: .key),
value: try nestedContainer.decode(Int.self, forKey: .value))
case .eraseAll:
_ = try container.nestedUnkeyedContainer(forKey: .eraseAll)
self = .eraseAll
}
}

how to use Codable with Class enum

I feel like I'm going way too far for what should really be a simple thing. With the below code, the error that I get is: Cannot invoke 'decode' with an argument list of type '(GameOfLife.Cell, forKey: GameOfLife.Cell.CodingKeys)'
extension GameOfLife {
enum Cell: Equatable, Codable {
case alive
case born
case dying
case dead
var isAlive: Bool {
switch self {
case .alive, .born: return true
case .dying, .dead: return false
}
}
var isDead: Bool {
switch self {
case .alive, .born: return false
case .dying, .dead: return true
}
}
func equalTo(_ rhs: Cell) -> Bool {
switch (self) {
case .alive, .born:
return rhs.isAlive
case .dead, .dying:
return rhs.isDead
}
}
init(_ living: Bool) {
self = living ? .alive : .dead
}
enum CodingKeys: CodingKey {
case alive
case born
case dying
case dead
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
let leftValue = try container.decode(GameOfLife.Cell.alive, forKey: CodingKeys.alive)
self = GameOfLife.Cell.alive
} catch {
let leftValue = try container.decode(GameOfLife.Cell.born, forKey: CodingKeys.born)
self = GameOfLife.Cell.born
} catch {
let leftValue = try container.decode(GameOfLife.Cell.dying, forKey: CodingKeys.dying)
self = GameOfLife.Cell.dying
} catch {
let leftValue = try container.decode(GameOfLife.Cell.dead, forKey: CodingKeys.dead)
self = GameOfLife.Cell.dead
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .alive:
try container.encode("alive", forKey: .alive)
case .born:
try container.encode("born", forKey: .born)
case .dying:
try container.encode("dying", forKey: .dying)
case .dead:
try container.encode("dead", forKey: .dead)
}
}
}
}
The first parameter of decode( is the expected type of the decoded object. As you are encoding the enum cases a String, it's supposed to be String
let leftValue = try container.decode(String.self, forKey: .alive) // The compiler can infer the type `CodingKeys`
However the entire init(from method makes no sense. If an error occurs inside a catch expression it is not going to be caught in the subsequent catch expression.
To encode an enum case as String just declare the raw type of the enum as String and delete the CodingKeys and the init(from and encode(to methods. And if you want to adopt Equatable you have to implement ==
extension GameOfLife : Codable {
enum Cell: String, Equatable, Codable {
case alive, born, dying, dead
var isAlive: Bool {
switch self {
case .alive, .born: return true
case .dying, .dead: return false
}
}
// isDead can be simplified
var isDead: Bool {
return !isAlive
}
static func == (lhs: Cell, rhs: Cell) -> Bool {
switch (lhs.isAlive, rhs.isAlive) {
case (true, true), (false, false): return true
default: return false
}
}
init(_ living: Bool) {
self = living ? .alive : .dead
}
}
}