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
How to Convert a View (Not Uiview) to an Image
Swift 2 - Pattern Matching in "If"
Closure With Generic Parameters
Arkit - What Do the Different Columns in Transform Matrix Represent
Removeobjectsatindexes For Swift Arrays
Express For Loops in Swift With Dynamic Range
Value of Type 'Storagemetadata' Has No Member 'Downloadurl'
Swift - Spritekit Cgpoint Alignment
What Is Geometry Reader in Swiftui
Getting "File Not Found" in Bridging Header When Importing Objective-C Frameworks into Swift Project
Can You Execute an Applescript Script from a Swift Application
How to Remove Diacritics from a String in Swift
Instance Member Cannot Be Used on Type
How to Get the Current Date in Short Format in Swift