The opinions expressed on this blog are mine alone

Swift: decoding JSON objects and class inheritcance

2023-03-19

Let say we have JSON data, where the different top level objects share keys. In fact, these top level objects could be described with Object Oriented paradigms.

                  
{
  "model_s": {
    "price": 89990
  },
  "plaid": {
    "price": 109990,
    "range": 396
  }
}
                  
                

Now, we would like to decode this JSON structure into classes, by writing resuable code and without abusing Swift too much. Let's define the base class:

                    
class ModelS: Codable {

    var price: Int

    private enum CodingKeys: String, CodingKey {
        case price
    }
}
                    
                  

So far so good. For the base class, we can get away with this minimal setup, as the compiler will do the heavy lifting. Now let's see the subclass:

                  
class Plaid: ModelS {

    var range: Int

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.range = try container.decode(Int.self, forKey: .range)

        try super.init(from: decoder)
    }

    private enum CodingKeys: String, CodingKey {
        case range
    }
}
                  
                

As you can see, we needed to implement required init(from:) for the subclass. This way we tell the compiler what to do with the range variable, and we also call super.init(from:) respectively. In case we did not implement this function, we would have to assign a default value for the range variable. During decoding, the compiler would just fill range with the default value.

In order to be able to decode the top level JSON objects into the right hierarchy, let's create a "wrapper" class:

                  
class Garage: Codable {

    var models: ModelS

    var plaid: Plaid

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.models = try ModelS(from: container.superDecoder(forKey: .models))
        self.plaid = try Plaid(from: container.superDecoder(forKey: .plaid))
    }

    private enum CodingKeys: String, CodingKey {
        case models = "model_s", plaid
    }
}
                  
                

Please note how the constructors get passed a superDecoder, for which we define the desired top-level keys. Now let's see the acutal decoding part:

                  
let JSON = """
{
  "model_s": {
    "price": 89990
  },
  "plaid": {
    "price": 109990,
    "range": 396
  }
}
"""

let jsonData = JSON.data(using: .utf8)!
let garage = try! JSONDecoder().decode(Garage.self, from: jsonData)
print("Model S price: \(garage.models.price)")
print("Model S Plaid price: \(garage.plaid.price) range: \(garage.plaid.range)")
                  
                

Running the above code shows that we can declare success:

Swift version 5.7.3 (swift-5.7.3-RELEASE)                                     23:56:09 17s
Target: x86_64
Model S price: 89990
Model S Plaid price: 109990 range: 396
                  

The whole code:

                  
import Foundation

class Garage: Codable {

    var models: ModelS

    var plaid: Plaid

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.models = try ModelS(from: container.superDecoder(forKey: .models))
        self.plaid = try Plaid(from: container.superDecoder(forKey: .plaid))
    }

    private enum CodingKeys: String, CodingKey {
        case models = "model_s", plaid
    }
}

class ModelS: Codable {
    
    var price: Int

    private enum CodingKeys: String, CodingKey {
      case price
    }
}

class Plaid: ModelS {
   
    var range: Int

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.range = try container.decode(Int.self, forKey: .range)

        try super.init(from: decoder)
    }

    private enum CodingKeys: String, CodingKey {
      case range
    }
}

let JSON = """
{
  "model_s": {
    "price": 89990
  },
  "plaid": {
    "price": 109990,
    "range": 396
  }
}
"""

let jsonData = JSON.data(using: .utf8)!
let garage = try! JSONDecoder().decode(Garage.self, from: jsonData)
print("Model S price: \(garage.models.price)")
print("Model S Plaid price: \(garage.plaid.price) range: \(garage.plaid.range)")