9

How to customize automatic synthesizing Codable for enums with associated values

 2 years ago
source link: https://sarunw.com/posts/how-to-customize-automatic-synthesizing-codable-for-enums-with-associated-values/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to customize automatic synthesizing Codable for enums with associated values

13 Jan 2022 ⋅ 5 min read ⋅ Swift Enum Codable

Table of Contents

Part 2 in a series on "Codable synthesis for enums with associated values". In the first part, we learn how easy it is to make enums with associated values conform to Codable protocol. This article will explore how much we can customize synthesized code to fit our needs.


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

codeshot.png Sponsor sarunw.com and reach thousands of iOS developers.

Synthesized code

To customize default behavior, we must first understand what happened behind the scene.

We will use the same Role enum from our previous example.

enum Role: Codable {
case admin
case guest(String?)
case member(id: String)
case vipMember(id: String, Int)
}

We learned from the previous article that it will encoded into a nested structure like this.

{
"admin" : {}
}

{
"guest" : {
"_0" : "John"
}
}

{
"member" : {
"id" : "1234"
}
}

{
"vipMember" : {
"id" : "1234",
"_1" : 5
}
}

I think it is easier to think of it in the form of a struct where each case is another struct with associated values as properties.

Here are similar structs which can represent the same JSON structure. This is for demonstration purposes only. It is not how synthesis work.

struct AdminStruct: Codable {}

struct GuestStruct: Codable {
let _0: String?
}

struct MemberStruct: Codable {
let id: String
}

struct VipMemberStruct: Codable {
let id: String
let _1: Int
}

struct RoleStruct: Codable {
let admin: AdminStruct?
let guest: GuestStruct?
let member: MemberStruct?
let vipMember: VipMemberStruct?
}

Given that enums are encoded into a nested structure, there are multiple CodingKeys declarations.

  • One that contains the keys for each of the enum cases, which as before, is called CodingKeys.
  • One for each enum case that contains the keys for the associated values. These CodingKeys are prefixed with the capitalized case name, e.g. AdminCodingKeys for case admin.

The compiler would generate something like this.

extension Role: Codable {
// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
case admin
case guest
case member
case vipMember
}

// contains keys for all associated values of `case admin`
enum AdminCodingKeys: CodingKey {

}

// contains keys for all associated values of `case guest`
enum GuestCodingKeys: CodingKey {
case _0
}

// contains keys for all associated values of `case member`
enum MemberCodingKeys: CodingKey {
case id
}

// contains keys for all associated values of `case vipMember`
enum VipMemberCodingKeys: CodingKey {
case id
case _1
}
}

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

codeshot.png Sponsor sarunw.com and reach thousands of iOS developers.

Customization

We have learned that enum with associated values encoded into the nested structure and generated multiple CodingKeys under the hood.

You can think of it like a nested struct. So you can customize it just like how you did with a struct.

Map case to other names

You can map any case to a different name by specifying a string value to CodingKeys case. In this case, vipMember will be mapped to the vip.

extension Role: Codable {
// contains keys for all cases of the enum
enum CodingKeys: String, CodingKey {
case admin
case guest
case member
case vipMember = "vip"
}
...
}

Role.vipMember(id: "1234", 5) will encode to the following JSON.

{
"vip" : {
"id" : "1234",
"_1" : 5
}
}

Map value to other names

You can map any value key by specifying a string value, but instead of doing it on CodingKeys, you do this on an enum case coding keys.

In this case, numberOfYears will be mapped to the second value of vipMember(id: String, Int). Notice that we need to do this on corresponding coding keys, VipMemberCodingKeys.

extension Role: Codable {
...
enum VipMemberCodingKeys: String, CodingKey {
case id
case _1 = "numberOfYears"
}
}

Role.vipMember(id: "1234", 5) will encode to the following JSON.

"vipMember" : {
"id" : "1234",
"numberOfYears" : 5
}

Exclude cases

You can control which cases in an enum should be codable by modifying CodingKeys declarations. You can exclude any case by removing it from the CodingKeys declaration.

In this case, we remove vipMember from CodingKeys. The compiler will no longer synthesize the code for vipMember.

extension Role: Codable {
// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
case admin
case guest
case member
// case vipMember
}
...
}

An attempt to encode or decode a vipMember value will cause an error to be thrown, so make sure you catch an operation or make it optional.

Encode vipMember.

let role = Role.vipMember(id: "1234", 5)        
let jsonData = try! JSONEncoder().encode(role)
// Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Swift.EncodingError.invalidValue(enum_codable.Role.vipMember(id: "1234", 5), Swift.EncodingError.Context(codingPath: [], debugDescription: "Case \'vipMember\' cannot be decoded because it is not defined in CodingKeys.", underlyingError: nil))

Decode to vipMember.

let jsonString = """
{
"vipMember" : {
"id" : "1234",
"_1" : 5
}
}
"""
let role = try! JSONDecoder().decode(Role.self, from: jsonString.data(using: .utf8)!)
// Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(enum_codable.Role, Swift.DecodingError.Context(codingPath: [], debugDescription: "Invalid number of keys found, expected one.", underlyingError: nil))

Exclude values

You can control which associated value in an enum case should be codable by modifying corresponding CodingKeys.

You can safely exclude any value if you only conform to Encodable. The excluded value will not be encoded to a JSON.

Role only conforms to Encodable here.

extension Role: Encodable {
...
enum VipMemberCodingKeys: CodingKey {
case id
// case _1
}
}

Role.vipMember(id: "1234", 5) will encode to.

{
"vipMember" : {
"id" : "1234"
}
}

Things get a little complicated with Decodable. Excluding any value mean the system won't know how to decode JSON to that particular value. This results in a compile error.

Type 'Role' does not conform to protocol 'Decodable' error. Type 'Role' does not conform to protocol 'Decodable' error.

To fix the problem, values that are excluded must have a default value defined.

Add a default value to the excluded value (the second value) of vipMember to satisfied Decodable conformance and fix the error.

enum Role {
case admin
case guest(String?)
case member(id: String)
case vipMember(id: String, Int = 1)
}

extension Role: Codable {
enum VipMemberCodingKeys: CodingKey {
case id
// case _1
}
...
}

You may also like

Codable synthesis for enums with associated values in Swift 5.5

Swift 5.5 extends the support for the automatic synthesis to enums with associated values. Learn what can be expected from the synthesis code.

Swift Enum Codable
Decode an array with a corrupted element

When working with an unstable, legacy, or third party API, you might get a malformed object in an array. Learn how to decode a JSON array with corrupted data in Codable safely.

Swift Codable

Read more article about Swift, Enum, Codable,

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
How to quickly test apps in other languages with an Xcode scheme

A tip for creating multiple schemes to quickly run your app in different languages.

Next
How to test UI layout for different languages with Pseudolanguages

Each language has its own characteristic. Some are more verbose than others. Some have special characters that take up vertical spaces. Some even read and lay out from right to left. Let's see how to make sure your layout is ready for this.

← Home


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK