Enum & custom type from primitive JSON type

Swift Codable

⋅ 4 min read ⋅ Swift iOS Codable JSON Enum

Table of Contents

Last year I wrote a review of Codable protocol in Swift 4 and how it competed with JSON encoder/decoder out there. I am happy to say that Codable served me well over the year. Today, I'm going to share some more use cases that I used throughout the year, which also use Codable.

Enum

If your enum has a raw value that can be represented as Int or String (which I think that should cover most cases), we can make enum Codable by assigning a raw value to it.

Let’s say you have json object like this:

{"name": "iOS developer", "status": "open"}

You can create Swift struct like this:

struct Job: Codable {

enum Status: String, Codable {
case open
case close
}

let name: String
let status: Status
}

If you have enum which isn't String or Int representable, you can still make it conform to Codable as long as that raw value is Codable.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Custom type from a primitive type

If you have custom type that can derived from basic JSON type, for example you have object with image sending as base64 encoded string:

{
"name": "XXX",
"image": "data:image/png;base64,iVBORw0KGgo......."
}

Since UIImage doesn't conform toCodable and we can't conform it with an extension, we have two ways to handle this:

  1. Create another wrapper class around this image.
struct Base64ImageWrapper: Codable {
let image: UIImage

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let base64String = try container.decode(String.self)
let components = base64String.split(separator: ",")
if components.count != 2 {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Wrong data format")
}

let dataString = String(components[1])

if let dataDecoded = Data(base64Encoded: dataString, options: .ignoreUnknownCharacters),
let image = UIImage(data: dataDecoded) {
self.image = image
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Can't initialize image from data string")
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = image.jpegData(compressionQuality: 1)
let prefix = "data:image/jpeg;base64,"
guard let base64String = data?.base64EncodedString(options: .lineLength64Characters) else {
throw EncodingError.invalidValue(image, EncodingError.Context(codingPath: [], debugDescription: "Can't encode image to base64 string."))
}

try container.encode(prefix + base64String)
}
}

The code above should be familiar to you with one key take away.

Instead of :

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

we use:

let container = try decoder.singleValueContainer()

That's because we are dealing with one primitive value, base64 string, in this case.

Then we can use the wrapper like this:

struct ModelUsingImageWrapper: Codable {
let name: String
let image: Base64ImageWrapper
}
  1. Add custom encode/decode for UIImage in KeyedEncodingContainer :
extension KeyedEncodingContainer {

mutating func encode(_ value: UIImage,
forKey key: KeyedEncodingContainer.Key) throws {

let imageData = value.jpegData(compressionQuality: 1)
let prefix = "data:image/jpeg;base64,"

guard let base64String = imageData?.base64EncodedString(options: .lineLength64Characters) else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Can't encode image to base64 string."))
}

try encode(prefix + base64String, forKey: key)
}

}

extension KeyedDecodingContainer {

public func decode(_ type: UIImage.Type, forKey key: KeyedDecodingContainer.Key) throws -> UIImage {
let base64String = try decode(String.self, forKey: key)

// data:image/svg+xml;base64,PD.....
let components = base64String.split(separator: ",")

if components.count != 2 {
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Unsupported format"))
}

let dataString = String(components[1])
if let dataDecoded = Data(base64Encoded: dataString, options: .ignoreUnknownCharacters), let image = UIImage(data: dataDecoded) {
return image
} else {
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Unsupported format"))
}
}

}

And use this with explicit encode and decode.

struct ModelUsingKeyedEncodingContainer: Codable {
let name: String
let image: UIImage

enum CodingKeys: String, CodingKey {
case name
case image
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
image = try container.decode(UIImage.self, forKey: .image)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(image, forKey: .image)
}
}

These two approaches should suffice for most of your use cases.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Read more article about Swift, iOS, Codable, JSON, Enum, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
Codable in Swift 4.0

Can it replace JSON encode/decode lib out there?

Next
Introduction to Coordinator

iOS flow controller

← Home