Codable observable objects

Diego Lavalle
Swift You and I
Published in
2 min readAug 1, 2020

Complying to the Codable protocol is simple thanks to synthesized initializers and coding keys. Similarly making your class observable using the Combine framework is trivial with ObservableObject. But attempting to merge these two protocols in a single implementation poses a few obstables. Let’s find out!

Codable observable objects

A simple class can be made Encodable and Decodable simultaneously by simply declaring it as Codable.

class CodableLandmark: Codable {  var site: String = "Unknown site"
var visited: Bool = false
}

After this you can easily convert objects to JSON format and back.

import Foundationclass CodableLandmark: Codable {


// Encode this instance as JSON data
var asJsonData: Data? {
try? JSONEncoder().encode(self)
}

// Create an instance from JSON data
static func fromJsonData(_ json: Data) -> Self? {
guard let decoded = try? JSONDecoder().decode(Self.self, from: json) else {
return nil
}
return decoded
}
}

An identical class could alternatively serve as a model for some SwiftUI view simply by adhering to ObservableObject. Annotating properties with the @Published wrapper will ensure that notifications are generated when these values change.

import Combineclass ObservableLandmark: ObservableObject {  @Published var site: String = "Unknown site"
@Published var visited: Bool = false
}

Initially just adding the ObservableObject to our codable class’ protocol list produces no error. But when we try to mark the first variable as published we encounter error messages Type 'LandmarkModel' does not conform to protocol 'Decodable' and Type 'LandmarkModel' does not conform to protocol 'Encodable' error.

class LandmarkModel: Codable, ObservableObject {  @Published // Error
var site: String = "Unknown site"

@Published // Error
var visited: Bool = false
}

The solution is to simply implement some of the Codable requirements manually rather than have them synthesized. Namely the initializer (for decoding) and the encode function for encoding. This forces us to additionally declare our coding keys explicitly.

class LandmarkModel: Codable, ObservableObject {
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case site
case visited
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
site = try values.decode(String.self, forKey: .site)
visited = try values.decode(Bool.self, forKey: .visited)
}
// MARK: - Encodable
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(site, forKey: .site)
try container.encode(visited, forKey: .visited)
}
}

This approach of tightly coupling the codable and observable models can be useful for simple projects as it avoids a lot of boilerplate code.

Check out the associated Working Example to see this technique in action.

FEATURED EXAMPLE

Real-Time JSON Observe the codable object

--

--

Diego Lavalle
Swift You and I

Mad computer scientist. Builder of Apps. Early adopter. Web Standards and Apple Frameworks specialist.