Codable Enum with Multiple Keys and Associated Values

Codable enum with multiple keys and associated values

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

Making an enum with associated values conform to decodable?

You need to create a custom init(from decoder: Decoder) throws, and treat your associated values like they are normal fields in the object, using CodingKeys to decode them. Details depend on how your associated values are actually represented in encoded object.

For example if your values are encoded as:

{ "team1": "aaa", "team2": "bbb" }

and

{ "individual": ["x", "y", "z"]}

You can:

enum SportType: Decodable {

case team(String, String) //Two team names
case individual([String]) //List of player names

// Define associated values as keys
enum CodingKeys: String, CodingKey {
case team1
case team2
case individual
}

init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

// Try to decode as team
if let team1 = try container.decodeIfPresent(String.self, forKey: .team1),
let team2 = try container.decodeIfPresent(String.self, forKey: .team2) {
self = .team(team1, team2)
return
}

// Try to decode as individual
if let individual = try container.decodeIfPresent([String].self, forKey: .individual) {
self = .individual(individual)
return
}

// No luck
throw DecodingError.dataCorruptedError(forKey: .individual, in: container, debugDescription: "No match")
}
}

Test:

let encoded = """
{ "team1": "aaa", "team2": "bbb" }
""".data(using: .utf8)

let decoded = try? JSONDecoder().decode(SportType.self, from: encoded!)

switch decoded {
case .team(let a, let b):
print("I am a valid team with team1=\(a), team2=\(b)")
case .individual(let a):
print("I am a valid individual with individual=\(a)")
default:
print("I was not parsed :(")
}

Prints:

I am a valid team with team1=aaa, team2=bbb

Make Enum (with associated type) Codable, successfully

I think you should first check whether a key exists in the decoder's container or not, before decoding it. Currently, you are doing this:

do {
let value = try values.decode(Double.self, forKey: .pulse)
self = .pulse(value)
} catch (let error) {
print(error)
}

do {
let value = try values.decode(Double.self, forKey: .draw)
self = .draw(value)
} catch (let error) {
print(error)
}

do {
let value = try values.decode(Double.self, forKey: .flash)
self = .flash(value)
} catch (let error) {
print(error)
}

There is no way that the decoder container is going to have all three keys in there, isn't it?

So, check before decoding:

if values.contains(.pulse) {
do {
let value = try values.decode(Double.self, forKey: .pulse)
self = .pulse(value)
return // remember to return here, so you don't set self back to .draw(0.0) again!
} catch (let error) {
print(error)
}
} else if values.contains(.draw) {
do {
let value = try values.decode(Double.self, forKey: .draw)
self = .draw(value)
return
} catch (let error) {
print(error)
}
} else if values.contains(.flash) {
do {
let value = try values.decode(Double.self, forKey: .flash)
self = .flash(value)
return
} catch (let error) {
print(error)
}
}

How to use Codable protocol for an enum with other enum as associated value (nested enum)

The associated value is LoggedInState or LoggedOutState but you have to encode its rawValue (String):

    case let .LoggedIn(value):
try container.encode(value.rawValue, forKey: .loggedIn)
case let .LoggedOut(value):
try container.encode(value.rawValue, forKey: .loggedOut)
}

according to the decode method where you're creating the enum cases from the rawValue.

Swift codable recursive enum with dynamic keys

You can use singleValueContainer to decode/encode the each case of EntryData, without using any hardcoded keys. When decoding, we can try to decode as all three cases, and see which one succeeded.

enum EntryData: Codable {
case string(String)
case array([EntryData])
case nested([String: EntryData])

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let nested = try? container.decode([String: EntryData].self)
let array = try? container.decode([EntryData].self)
let string = try? container.decode(String.self)
switch (string, array, nested) {
case (let s?, nil, nil):
self = .string(s)
case (nil, let a?, nil):
self = .array(a)
case (nil, nil, let n?):
self = .nested(n)
default:
throw DecodingError.valueNotFound(
EntryData.self,
.init(codingPath: decoder.codingPath,
debugDescription: "Value must be either string, array or a dictionary!",
underlyingError: nil))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let s):
try container.encode(s)
case .array(let a):
try container.encode(a)
case .nested(let n):
try container.encode(n)
}
}
}

Now you can do:

let entry = try JSONDecoder().decode(Entry.self, from: data)

How to make a Swift Enum with Associated values conform to Codable?

You just need to switch your enumeration associated values:

enum Value: Codable {
case str(String)
case formField(FormField)
private enum CodingKeys: String, CodingKey {
case str, formField
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .str(string):
try container.encode(string, forKey: .str)
case let .formField(field):
try container.encode(field, forKey: .formField)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch container.allKeys.first {
case .str:
self = try .str(container.decode(String.self, forKey: .str))
case .formField:
self = try .formField(container.decode(FormField.self, forKey: .formField))
default:
throw DecodingError.dataCorrupted(
.init(
codingPath: container.codingPath,
debugDescription: "invalid data"
)
)
}
}
}

Swift enum associated value with different types

I would solve this with a Kind enum type, for each kind of variation.

public enum AnimationType {
public enum Kind<Value> {
case scalar(Value)
case keyframes([Keyframe<Value>])
}

case position(Kind<Float>)
case scale(Kind<Float>)
case rect(Kind<CGRect>)
case transform(Kind<CGAffineTransform>)
}

Usage:

let anim1 = AnimationType.position(.scalar(10))
let anim2 = AnimationType.position(.keyframes([Keyframe(10)]))

Getting values:

switch anim1 {
case .position(let kind):
switch kind {
case .scalar(let value):
print("value: \(value)")
case .keyframes(let keyframes):
print("keyframes: \(keyframes)")
}

default: // You would implement the rest
break
}
switch anim1 {
case .position(.scalar(let value)):
print("value: \(value)")

case .position(.keyframes(let keyframes)):
print("keyframes: \(keyframes)")

default: // You would implement the rest
break
}
if case .position(.scalar(let value)) = anim1 {
print("value: \(value)")
}

You can also add Codable conformance:

public struct Keyframe<Value: Codable> {
let value: Value

init(_ value: Value) {
self.value = value
}
}

extension Keyframe: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
value = try container.decode(Value.self)
}
}
public enum AnimationType {
public enum Kind<Value: Codable> {
case scalar(Value)
case keyframes([Keyframe<Value>])
}

case position(Kind<Float>)
case scale(Kind<Float>)
case rect(Kind<CGRect>)
case transform(Kind<CGAffineTransform>)
}

extension AnimationType.Kind: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()

switch self {
case .scalar(let value): try container.encode(value)
case .keyframes(let keyframes): try container.encode(keyframes)
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let scalar = try? container.decode(Value.self) {
self = .scalar(scalar)
return
}
if let keyframes = try? container.decode([Keyframe<Value>].self) {
self = .keyframes(keyframes)
return
}

// You should throw error here instead
fatalError("Failed to decode")
}
}

extension AnimationType: Codable {
private enum CodingKeys: CodingKey {
case position
case scale
case rect
case transform
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .position(let kind): try container.encode(kind, forKey: .position)
case .scale(let kind): try container.encode(kind, forKey: .scale)
case .rect(let kind): try container.encode(kind, forKey: .rect)
case .transform(let kind): try container.encode(kind, forKey: .transform)
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

if let position = try? container.decode(Kind<Float>.self, forKey: .position) {
self = .position(position)
return
}
if let scale = try? container.decode(Kind<Float>.self, forKey: .scale) {
self = .scale(scale)
return
}
if let rect = try? container.decode(Kind<CGRect>.self, forKey: .rect) {
self = .rect(rect)
return
}
if let transform = try? container.decode(Kind<CGAffineTransform>.self, forKey: .transform) {
self = .transform(transform)
return
}

// You should throw error here instead
fatalError("Failed to decode")
}
}

Example encoding:

do {
let data = try JSONEncoder().encode(anim1)
if let str = String(data: data, encoding: .utf8) {
print(str)
// Prints: {"position":10}
}
} catch {
print(error)
}

The same sort of thing with anim2 returns {"position":[10]}.

How to make a Codable class with multiple possible enum types?

You need to make the Status type generic so that compiler knows you will pass a concrete type that conforms to protocol Status so it is decodable & encodable.

class ProdEvent<T: Status> : Codable {
let status : T
}

class MyCollection<T: Status> : Codable {
let arr_events : [ProdEvent<T>]
}

Now, you can make ProdEvent and MyCollection object of any kind of Status type as below,

var aEvent: ProdEvent<StatusA>!
var bEvent: ProdEvent<StatusB>!

var aCollection: MyCollection<StatusA>!
var bCollection: MyCollection<StatusB>!

You can check this thread as well on why you can't use protocol as a concrete type.



Related Topics



Leave a reply



Submit