Codable, introduced in Swift 4, provides a convenient and easy way to encode and decode JSON. However, not everything works out of the box.
For instance, let's look at this JSON file from Twitter. As a result of the way the JSON decoder works, we see:
- **Quoted and Standard Tweets use similar but not exactly the same fields.**
- **Keys which get translated to property names don't follow Swift conventions.**
- Dates are not in the standard accepted format.
- Colors are not in a format accepted by any Color class (UIColor, NSColor, etc...)
As a result of this, we are going to learn how to setup your Codable types for some of those special cases. Specifically, we are going to look at:
- Using Protocols with Codable types
- Different Property Names and Keys
- Dates in Property Values
- Property Values for Custom Types
To follow along you can look at the Xcode playground for this article here at GitHub.
How JSONDecoder works
These are basic rules for how the JSONDecoder converts JSON into a structured type:
- Property names are converted as is.
- Simple property values by default are converted to
String
,Int
, orDouble
- Property values which follow the correct format may be decoded into
URL
,Data
, orDate
. - Any property value may be assumed to be
Optional
- Square bracket
[]
values are converted toArray
- Curly brackets
{}
values are convert toDictionary
or a custom type. - All types which need to be decoded need to implement the Decodable protocol. Likewise, types which need to be encoded need to implement the Encodable protocol. If the type needs to be both decoded and encoded can simply implement Codable.
In this case, we’ll be looking at the Twitter JSON data returned for a tweet. Therefore, the first thing we are going to do is simply create some basic structures for each type of data returned. As a result, you can look at the first step of working decoding here on this branch of the repo.
Even if the data is similar, we create separate structs in order to allow more flexibility. However, that does not mean we can’t use protocols in order simplify the consumption of those types in functions.
Using Protocols with Codable
For instance, in the Twitter API, the notion of a tweet is used throughout. As an example, their JSON has a main tweet as well as quoted tweets. Therefore, one might be inclined to use the same structure for both. However, a better approach is two separate structures:
public struct Tweet : Codable {
public let created_at : Date
public let id : Int
public let full_text : String
public let display_text_range : [Int]
public let entities : TweetEntities
public let source : String
public let in_reply_to_status_id : Int?
public let in_reply_to_user_id : Int?
public let in_reply_to_screen_name : String
public let user : TweetUser
public let quoted_status : QuotedTweet?
public let is_quote_status : Bool
public let retweet_count : Int
public let favorite_count : Int
public let favorited : Bool
public let retweeted : Bool
public let possibly_sensitive : Bool
public let possibly_sensitive_appealable : Bool
public let lang : String
}
public struct QuotedTweet : Codable {
public let created_at : Date
public let id : Int
public let full_text : String
public let display_text_range : [Int]
public let entities : TweetEntities
public let user : TweetUser
public let source : String
public let extended_entities : TweetEntities
public let is_quote_status : Bool
public let retweet_count : Int
public let favorite_count : Int
public let favorited : Bool
public let retweeted : Bool
public let possibly_sensitive : Bool
public let possibly_sensitive_appealable : Bool
public let lang : String
}
Therefore, this means duplicated fields. However, it also allows for better flexibility and easier JSON decoding. On the other hand, we can simplify these types when used as parameters in functions. For instance, let’s say we need to print the tweet out:
func printTweet (_ tweet: Tweet) {
print(tweet.full_text)
if let quoted_status = tweet.quoted_status {
printTweet(quoted_status)
}
}
func printTweet (_ tweet: QuotedTweet) {
print(">",tweet.full_text)
}
However, using some basic Protocol-Oriented Programming, we can in fact be optimize this. Firstly, we stub a function which prints out the tweet based on a protocol regardless of whether it is a quoted tweet or not.
func printTweet(_ tweet: TweetProtocol, withQuoteLevel level: Int = 0) {
print(String(repeating: ">", count: level),tweet.full_text)
if let quotedTweet = tweet.quotedTweet {
printTweet(quotedTweet, withQuoteLevel: level+1)
}
}
Consequently, we create the protocol which has the properties we need for the function above.
public protocol TweetProtocol {
var full_text : String { get }
var quotedTweet : TweetProtocol? { get }
}
Lastly, we implement the protocol for the two types we’ll use it for -
Tweet
and QuotedTweet
:
extension Tweet : TweetProtocol {
public var quotedTweet: TweetProtocol? {
return self.quoted_status
}
}
extension QuotedTweet : TweetProtocol {
public var quotedTweet: TweetProtocol? {
return nil
}
}
Now we have better flexibility to work with Codable while at the same time adding similar functionality using Protocols. Next, let's cleanup the property names.
Custom Property Names
One thing you notice about Twitter’s JSON is that they use what’s called snake-case for combining words in property names. As a result, underscores _
are used for combining words in JSON keys. For instance, the property for the full text of a tweet is called full_text
. However, the Swift API design guidelines recommend using camel case. Camel case capitalizes each new word. Consequently, our property name would be in this case would be fullText
. Therefore, we have two ways of fixing this: custom CodingKeys or using a KeyEncodingStrategy.
Custom CodingKeys allows us the most flexibility by allowing us to provide a map from JSON keys to property names. However, in this case there is a consistent strategy for mapping all keys to property names. Therefore, in this case, we'll use KeyDecodingStrategy
specifically KeyDecodingStrategy.convertFromSnakeCase
:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
In addition, if KeyDecodingStrategy.convertFromSnakeCase
doesn’t quite fit with your JSON keys, Swift does allow the use of custom strategies. In other words, you can supply a closure for converting a CodingKey array to single CodingKey result.
Last but not least, if none of this strategies work for converting a key to a property name you can always supply a custom CodingKeys enumeration. In short, you want to use the simplest and most consistent strategy for mapping property names or JSON keys.
Custom Property Values
Another quirk with Twitter’s JSON data is how they format some of the property values. Let’s take a look at the easiest to deal with which is dates.
How to deal with Date
By default, dates are serialized as a floating decimal specifically TimeInterval (number of seconds) since the reference date of January 1st, 2001. However, if the format from the JSON property value is different, you can use one of the strategies available:
- iso8601 - a standard format used throughout the web
- secondsSince1970 - unix time in seconds
- millisecondsSince1970 - unix time in milliseconds
Consequently, if the date format does not follow any of these you need to provide either a closure to convert it or a DateFormatter. The main way to specify the format in DateFormatter is with the dateFormat String property. In other words, by using the Unicode provided codes, we can create a dateFormat string to represent the format of dates and times provided by Twitter. As a result, from the date
Mon Mar 28 14:39:13 +0000 2016
in our JSON, we can infer the format to be: eee MMM dd HH:mm:ss ZZZZ yyyy
. Therefore, we can create a DateFormatter and set the strategy accordingly:
let dateFormat = "eee MMM dd HH:mm:ss ZZZZ yyyy"
let dateFormatter = DateFormatter ()
dateFormatter.dateFormat = dateFormat
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
In conclusion, by customizing the DateFormatter, dates can be decoded as Date property values. However, colors will be slightly more challenging.
Custom Decoding of Property Values
For colors such as the profile background color, we need to create a separate struct and implement the encoding and decoding ourselves to convert the hexidecimal string to a usable color structure:
import Foundation
import CoreGraphics
public struct Color : Codable {
public let red : CGFloat
public let green : CGFloat
public let blue : CGFloat
public let alpha : CGFloat
public init(from decoder: Decoder) throws {
let hexCode = try decoder.singleValueContainer().decode(String.self)
let scanner = Scanner(string: hexCode)
var hexint : UInt32 = 0
scanner.scanHexInt32(&hexint)
self.red = CGFloat((hexint & 0xff0000) >> 16) / 255.0
self.green = CGFloat((hexint & 0xff00) >> 8) / 255.0
self.blue = CGFloat((hexint & 0xff) >> 0) / 255.0
self.alpha = 1
}
public func encode(to encoder: Encoder) throws {
let string = String(format: "%02lX%02lX%02lX", lroundf(Float(red * 255.0)), lroundf(Float(green * 255.0)), lroundf(Float(blue * 255.0)))
var container = encoder.singleValueContainer()
try container.encode(string)
}
}
In this case, we override the init method and grab the string from the
singleValueContainer
. After that, we use a Scanner to convert the color hexcode into a hexidecimal number. Then use a combination of bitwise and logical operations to grab each component value in between 0 and 255. Lastly, we use decimal division to calculate the value in between 0 and 1. (I have also included the encode method for completion sake.) As a result, we can easily convert a hexcode String to a Color struct.
Since we can't use the existing system color types, here is the code to convert from our custom Color struct to whatever the system's default UI color type is:
#if os(iOS) || os(watchOS) || os(tvOS)
import UIKit
public typealias SystemColor = UIColor
#elseif os(macOS)
public typealias SystemColor = NSColor
#endif
extension Color {
public var systemColor : SystemColor {
return SystemColor(red: self.red, green: self.green, blue: self.blue, alpha: self.alpha)
}
}
Make Codable Work For Us
Codable offers many layers of customability. However, in more case we want to use the least amount of customization with the most amount of consistency.
- For data used in similar ways **use separate types but implement the same protocol**
- If property names with a consistent strategy, **use a KeyEncodingStrategy rather than custom CodingKeys**
- For dates use dateDecodingStrategy, DateFormatter, or a closure.
- For other custom types, **create a separate type and customize the decoding** (and encoding) there.
If you have other issues you'd like me to look into, reach me at twitter. In addition, if you are interested in learning more about complex JSON decoding or encoding and other Swift topic, subscribe to my newsletter to stay up to date.
\*indicates required
Email Address \*
First Name
Last Name