Blog by Frank

Swift Codable, JSON, UserDefaults Explained

What’s JSON

JSON is a file format to store key-value pair.

It’s JSON for Pikachu

What’s Codable in the Swift

Codable was introduced in Swift 4.0, bringing with it incredibly smooth conversion between Swift data types and JSON.

The code comes from

Hacking With Swift by Paul Hudson

Codable cheat sheet

Convert between JSON and Swift types the smart way

Part I: Codable Part

Encoding and decoding JSON

import Foundation
let json = """
[
    {
        "name":"Frank",
        "age": 20
    },
    {
        "name": "Paul",
        "age": 38
    }
]
"""

/// Convert json into a **Data** object because that's 
/// what Codable decoders work with.
let data = Data(json.utf8)

/// Define a Swift Struct that will hold our finished data
struct User: Codable {
    var name: String
    var age: Int
}

/// An object that decodes instances of 
/// a data type from JSON objects.
let decoder = JSONDecoder()

do {
    let decoded = try decoder.decode([User].self, from: data)
    
    /// This will print "Frank", which is name of the firstusr in the JSON
    print(decoded[0].name)
} catch {
    print("Failed to decode JSON")
}

Converting case

Key is first_name in JSON, but we usually use firstName in Swift.

import Foundation

let json = """
[
    {
        "first_name": "Frank",
        "last_name": "Chu"
    },
    {
        "first_name": "Paul",
        "last_name": "Hudson"
    }
]
"""

let data = Data(json.utf8)

struct User: Codable {
    var firstName: String
    var lastName: String
}

/// To make this work we need to change only one property in the JSON decoder
let decoder = JSONDecoder()

/// That instructs Swift to map snake case names (`names_written_like_this`) to camel case names (`namesWrittenLikeThis`)
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let decoded = try decoder.decode([User].self, from: data)
    print(decoded)
} catch {
    print("Decode error")
}

Maping different key names

If you have JSON keys that are completely diffrent from your Swift properties, you can map them using a CodingKeys enum.

let json = """
[
    {
        "user_first_name": "Frank",
        "user_last_name": "Chu",
        "user_age": 26
    },
    {
        "user_first_name": "Paul",
        "user_last_name": "Hudson",
        "user_age": 26
    }
]
"""
let data = Data(json.utf8)

/// Those key names aren't great, and really we'd like to convert that data into a struct like this
struct User: Codable {
    var firstName: String
    var lastName: String
    var age: Int
    
    /// To make that happen, we need to declare a CodingKeys enum:
    /// a mapping that **Codable** can use to
    /// convert JSON names into properties for our struct.
    ///
    /// It needs to conform to the **CodingKey** protocol,
    /// which is what makes this work with **Codable** protocol.
    enum CodingKeys: String, CodingKey {
        case firstName = "user_first_name"
        case lastName = "user_last_name"
        case age = "user_age"
    }
}

let decoder = JSONDecoder()

do {
    let decoded = try decoder.decode([User].self, from: data)
    print(decoded)
} catch {
    print(error)
}

Parsing hierarchical data the easy way

Any non-trival JSON is like to have hierarchical data - one collection of data nested inside another.

import Foundation

let json = """
[
    {
        "name": {
            "first_name": "Taylor",
            "last_name": "Swift"
        },
        "age": 26
    },
    {
        "name": {
            "first_name": "Frank",
            "last_name": "Chu"
        },
        "age": 20
    }
]
"""

let data = Data(json.utf8)

/// **Codable** is able to handle this just fine, as long as you can describle the relationships clearly.
/// This is the easy way to do this is using nested structs.
struct User: Codable {
    struct Name: Codable {
        var firstName: String
        var lastName: String
    }
    
    var name: Name
    var age: Int
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let decoded = try decoder.decode([User].self, from: data)
    
    /// The downside is that if you want to read a user's first name,
    /// you need to use `user.name.first`,
    /// but at least the actual parsing work is trivial
    /// - our existing code works already!
    if let user = decoded.last {
        print(user.name.firstName)
    }
} catch {
    print(error)
}

Parsing hierarchical data the hard way

If you want to parse hierarchical data into a flat struct - i.e., you want to be able to write user.firstName rather than user.name.firstName - then you need to do some parsing yourself.

import Foundation

/// Second, we need to define coding keys
/// that describe where data can be found in the hierarchy.
let json = """
[
    {
        "name": {
            "first_name": "Taylor",
            "last_name": "Swift"
        },
        "age": 26
    },
    {
        "name": {
            "first_name": "Frank",
            "last_name": "Chu"
        },
        "age": 20
    }
]
"""

let data = Data(json.utf8)

/// First, create the struct you want to end up with
struct User: Codable {
    var firstName: String
    var lastName: String
    var age: Int
    
    /// As you can see, at the root there's a key
    /// called "name" and another called "age",
    /// "so" we need to add that as our root coding keys.
    enum CodingKeys: String, CodingKey {
        case name, age
    }
    
    /// Inside "name" were two more keys, `"first_name"` and `"last_name"`,
    /// so we're going to create some coding keys for those two.
    enum NameCodingKeys: String, CodingKey {
        case firstName, lastName
    }
    
    /// Now for the hard part: we need to write a custom initializer and
    /// custom encode method for our type.
    init(from decoder: Decoder) throws {
        /// Inside there, the first thing we need to do is
        /// attempt to pull out a container we can read
        /// using the keys of our **CodingKeys** enum
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        /// Once that's done we can attempt to read our age property.
        age = try container.decode(Int.self, forKey: .age)
        
        /// Next we need to dig down one level to read our name data.
        let name = try container.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
        
        firstName = try name.decode(String.self, forKey: .firstName)
        lastName = try name.decode(String.self, forKey: .lastName)
    }
    
    /// `encode(to:)` is effectively the reverse of the initializer we just write.
    func encode(to encoder: Encoder) throws {
        var containder = encoder.container(keyedBy: CodingKeys.self)
        try containder.encode(age, forKey: .age)
        
        var name = containder.nestedContainer(keyedBy: NameCodingKeys.self, forKey: .name)
        try name.encode(firstName, forKey: .firstName)
        try name.encode(lastName, forKey: .lastName)
    }
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let decoded = try decoder.decode([User].self, from: data)
    print(decoded)
} catch {
    print(error)
}

Part II: UserDefaults Part