JSON and Codable

Her are some tips for using JSON and Codable.

Codable is a compound protocol containing Decodable & Encodable. When your type implements these, instances can be created from Decoder objects, and instances can be encoded out using Encoder objects.

JSONEncoders, using Inversion of Control, will create an Encoder and call your typeencode method. JSONDecoders likewise will create instances using the special init(from decoder: Decoder) method. Inversion of control means entire data graphs can be coded witha single call. Nicely automatic!

Here’s a sample round trip encode and decode:

Swift 4
// Encode a Thing
let jencoder = JSONEncoder()
do {
    let d1: Data = try jencoder.encode(thing)
    // d1 is now a Data which is a String in utf8 encoding.
	
    // Decode a Data back to a Thing
    let jdecoder = JSONDecoder()
    let sameAsThing = try jdecoder.decode(Thing.self, from: d1)
    // sameAsThing is now a Thing
}
catch let err {
    print(err.localizedDescription)
}

Here what the type looks like:

Swift 4
struct Thing: Codable {
    let parts: [String]
    init(_  parts: [String]) { self.parts = parts }
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        var parts = [String]()
        while !container.isAtEnd {
            let part = try container.decode(String.self)
            parts.append(part)
        }
        self.init(parts)
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        for part in parts {
            try container.encode(part)
        }
    }
}

Another way to encode an array is:

Swift 4
struct Thing: Codable {
    let parts: [String]
    init(_  parts: [String]) { self.parts = parts }
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let parts = try container.decode([String].self)
        self.init(parts)
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.parts)
    }
}

However, a top-levle JSON cannot be a scalar. It must be an encoded array or dictionary.

Swift 4
struct Thing: Codable {
    let parts: [String]
    init(_  parts: [String]) { self.parts = parts }
    init(from decoder: Decoder) throws {
        let raw = try decoder.singleValueContainer().decode(String.self)
        self.init(raw.split(separator: " ").map({ String($0) }))
    }
    func encode(to encoder: Encoder) throws {
        var ven = encoder.singleValueContainer()
        let raw = parts.joined(separator: " ")
        try ven.encode(raw)
    }
}

If you try to encode this, it tries to generate a top-level String, which is scalar and creates illegal JSON. You’ll get an invalidValue error:

  • debugDescription: "Top-level Text encoded as string JSON fragment.”
  • localizedDescription: "The data couldn’t be written because it isn’t in the correct format."