Codable observable objects

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!

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 Foundation

class 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 Combine

class 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