How to Make an Enum Decodable in Swift

How do I make an enum Decodable in Swift?

It's pretty easy, just use String or Int raw values which are implicitly assigned.

enum PostType: Int, Codable {
case image, blob
}

image is encoded to 0 and blob to 1

Or

enum PostType: String, Codable {
case image, blob
}

image is encoded to "image" and blob to "blob"


This is a simple example how to use it:

enum PostType : Int, Codable {
case count = 4
}

struct Post : Codable {
var type : PostType
}

let jsonString = "{\"type\": 4}"

let jsonData = Data(jsonString.utf8)

do {
let decoded = try JSONDecoder().decode(Post.self, from: jsonData)
print("decoded:", decoded.type)
} catch {
print(error)
}

Update

In iOS 13.3+ and macOS 15.1+ it's allowed to en-/decode fragments – single JSON values which are not wrapped in a collection type

let jsonString = "4"

let jsonData = Data(jsonString.utf8)
do {
let decoded = try JSONDecoder().decode(PostType.self, from: jsonData)
print("decoded:", decoded) // -> decoded: count
} catch {
print(error)
}

In Swift 5.5+ it's even possible to en-/decode enums with associated values without any extra code. The values are mapped to a dictionary and a parameter label must be specified for each associated value

enum Rotation: Codable {
case zAxis(angle: Double, speed: Int)
}

let jsonString = #"{"zAxis":{"angle":90,"speed":5}}"#

let jsonData = Data(jsonString.utf8)
do {
let decoded = try JSONDecoder().decode(Rotation.self, from: jsonData)
print("decoded:", decoded)
} catch {
print(error)
}

Decoding enum in Swift 5 - This CANNOT be this hard

This all you need here.

struct Contract: Codable {
var contractType: ContractType

enum ContractType: Int, Codable {
case scavenger
}
}

do {
let contract = try JSONDecoder().decode([Contract].self, from: json)
print(contract.contractType)
} catch {
print(error)
}

Codable enum with default case in Swift 4

You can extend your Codable Type and assign a default value in case of failure:

enum Type: String {
case text,
image,
document,
profile,
sign,
inputDate = "input_date",
inputText = "input_text" ,
inputNumber = "input_number",
inputOption = "input_option",
unknown
}
extension Type: Codable {
public init(from decoder: Decoder) throws {
self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}

edit/update:

Xcode 11.2 • Swift 5.1 or later

Create a protocol that defaults to last case of a CaseIterable & Decodable enumeration:

protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where RawValue: Decodable, AllCases: BidirectionalCollection { }

extension CaseIterableDefaultsLast {
init(from decoder: Decoder) throws {
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
}
}

Playground testing:

enum Type: String, CaseIterableDefaultsLast {
case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
}

let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8))  // [text, image, unknown]

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

How do you make an enum decodable by its case name and not its raw value?

I looked into it and the problem here is that what you see in the JSON result is an encoded value, not a key. Consequently, adding CodingKeys won't help.

A slightly complicated solution uses a custom protocol and a corresponding extension to achieve the goal.

With that, you can declare:

    enum Test: String, CaseNameCodable {
case one = "Number One"
case two = "Number Two"
}

and it would do what you need.

A complete working example is sketched below (works for me in a Playground in Xcode 11.2):

    import Foundation

// A custom error type for decoding...
struct CaseNameCodableError: Error {
private let caseName: String

init(_ value: String) {
caseName = value
}

var localizedDescription: String {
#"Unable to create an enum case named "\#(caseName)""#
}
}

//
// This is the interesting part:
//

protocol CaseNameCodable: Codable, RawRepresentable , CaseIterable {}

extension CaseNameCodable {

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self)
guard let raw = Self.allCases.first(where: { $0.caseName == value })?.rawValue else { throw CaseNameCodableError(value) }
self.init(rawValue: raw)!
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(caseName)
}

private var caseName: String {
return "\(self)"
}
}

//
// Now you can use the protocol CaseNameCodable just like you
// would use Codable (on RawRepresentable enums only)
//

enum Test: String, CaseNameCodable {
case one = "Number One"
case two = "Number Two"
}

// EXAMPLE:

// Create a test value
let testValue = Test.one

// encode it and convert it to a String
let jsonData = try! JSONEncoder().encode(testValue)
let jsonString = String(data: jsonData, encoding: .utf8)!

print (jsonString) // prints: "one"

// decode the same data to produce a decoded enum instance
let decodedTestValue = try JSONDecoder().decode(Test.self, from: jsonData)

print(decodedTestValue.rawValue) // prints: Number One

Decodable & Enum Values

decodeIfPresent returns nil if the key is not present, and it will likely throw an error if the value is invalid. By using try?, we can do this:

ratingType = 
(try? (container.decodeIfPresent(RatingType.self, forKey: .ratingType) ?? <some default value>)
) ?? <some default value>

Swift - How to use CodingKeys to map an Int to a custom enum?

You can simplify your code quite a lot, all you need is the below code. By saying that your enum is of type Int swift can synthesise the correct decoding code

struct Locomotive: Codable {
var id, name: String
var generation: Int
var livery: Livery?
}

enum Livery: Int, Codable {
case green = 1, red, blue, yellow
}


Related Topics



Leave a reply



Submit