Swift: Codable Encounters of the Enum Kind


In my everyday life I am a publisher working with tools like InDesign but also performing web development and in part experimenting and developing on iOS and Android.

Working with JSON is a passion of mine, but it's not one that I get to indulge full time. So I spend long periods of time away from Xcode and Swift during which I easily forget all the stuff I learnt about Codable aside from the basics.

But recently I returned to my JSON work that I started before the Codable protocol was added to Swift and started stripping out my own bespoke JSON approach and replacing it with Codable elements.

One question I had was regarding the behaviour in the situation where you have a codable enum with a raw type of String. What would happen if that enum was inserted as the type of a property where a string would be present in the JSON.

My hope was that if the string received matched the raw type of one of the enums then all would work well. Otherwise there would be more complicated work to be done through manual encoding and decoding.

The Results

The good news is that all worked as expected, if the string supplied by the JSON decoding (in this instance to the ListItemStyle type property), and it was valid, then all worked as expected but if the string supplied was not contained in the enum list (via raw type) then a ListItemStyle instance was not created.
enum ListStyle:String, Codable {
    case bullet, decimal, lower_alphabetical, upper_alphabetical, lower_roman, upper_roman, character, none
}

struct ListItemStyle: Codable {
    // character should be a string of a single character in length
    var type:ListStyle, character:String?
}
Here is the code for testing that:
let str = """
{"type":"bullet"}
"""
        let decoder = JSONDecoder()
        do {
            let cont = try decoder.decode(ListItemStyle.self, from: str.data(using: .utf8)!)
            print(cont.type)
        }
        catch {
            //
            print("no dice")
        }

More complicated requirements

In other situations we are not so lucky. For example, I had an instance where the value that would be supplied in JSON was either a string or a dictionary, and there is no way around this than to build your own decoder and encoder, because it's a step too far to expect Codable to figure this out for us in the same way it can understand the relationship between a simple raw type and an enum. But this isn't half as bad as it sounds.

The dictionary itself is Codable and contains only Codable types (not all provided here, but trust me on this they are):
struct ComponentTextStyle:Codable {
    var backgroundColor:ANColor?, dropCapStyle:DropCapStyle?, firstLineIndent:Int?, hangingPunctuation:Bool?, hyphenation:Bool?, lineHeight: Int?, linkStyle:TextStyle?, paragraphSpacingBefore:Int?, paragraphSpacingAfter:Int?, textAlignment:ComponentAlignment?, textShadow:Shadow?
   
}
Thankfully, we do not need to go through and resolve every key and value, as demonstrated in this associated enum:
indirect enum TextStyle:Codable {
    case named(String), formatted(ComponentTextStyle)
    
    func encode(to encoder: Encoder) throws {
        switch self {
        case .named(let str):
            try str.encode(to: encoder)
        case .formatted(let compTextStyle):
            try compTextStyle.encode(to: encoder)
            
        }
    }
    
    init(from decoder: Decoder) throws {
        do {
            let str = try String(from: decoder)
            self = TextStyle.named(str)
        }
        // if there is a type mismatch this means the json is valid but it doesn't meet the requires of our type, so we'll not rethrow the error but try again
        catch DecodingError.typeMismatch(_, _)  { }
        // Any other kind of error and we may as well rethrow it now because the json isn't valid
        catch {
            print("no dice")
            throw error
        }
        do {   
            let compTextStyle = try ComponentTextStyle(from: decoder)
            self = TextStyle.formatted(compTextStyle)
        }
        catch {
            print("no dice")
            throw error
        }
    }
}
The first thing you'll notice is that the keyword 'indirect' is used. This is because one of the values contained in the ComponentTextStyle type is itself TextStyle and this type of recursion isn't permitted without using the keyword. But aside from that all that is happening in these encode and decode methods is that the encoder or decoder is being passed along to the associated instances, and because they are both Codable they are able to handle this for us.

To test this the following can be done:
 let str = """
{"lineHeight":10}
"""
        let decoder = JSONDecoder()
        let encoder = JSONEncoder()
        do {
            let cont = try decoder.decode(TextStyle.self, from: str.data(using: .utf8)!)
            switch cont {
            case .named(let str):
                print(str)
            case .formatted(let comp):
                print(comp.lineHeight!)
                let enc = try encoder.encode(comp)
                print(String(data: enc, encoding: .utf8))
                
            }
        }
        catch {
            //
            print("no dice")
        }
        
    }
This tests with the pre-knowledge of a dictionary. If you wanted to test for a string in this isolated way, you would first need to enclose it within an array, because a lone string is not a valid piece of JSON. (Likewise when encoding back to JSON, the string once again needs to be contained in an array.)
      let str = """
["MyStyle"]
"""
        let decoder = JSONDecoder()
        let encoder = JSONEncoder()
        do {
            let cont = try decoder.decode([TextStyle].self, from: str.data(using: .utf8)!)
            switch cont[0] {
            case .named(let str):
                print(str)
                let enc = try encoder.encode([str])
                print(String(data: enc, encoding: .utf8))
            case .formatted(let comp):
                print(comp.lineHeight!)
            }
        }
        catch {
            //
            print("no dice")
        }
        
    }
This is rather a crude approach to testing but it demonstrates that this approach works and is performed with the knowledge that the TextStyle type will never provide the enclosing top-level instance.

Conclusion

Codable being able to handle the relationship between enums and raw types is beyond great in terms of saving time. And while there is inevitably going to be encounters with JSON where the value for a key is of a variable type (this is JSON after all!), being able to pass along responsibility to associated types for encoding removes the majority of the pain.

Comments