1

In my iOS Swift project, I need to convert objects to JSON.

I have one simple class:

class Car : Encodable
{
    var brand: String

    init(brand: String)
    {
        self.brand = brand
    }
}

and one subclass:

class SUVCar : Car
{
    var weight: Int

    init(_ weight: Int)
    {
        self.weight = weight
        super.init(brand: "MyBrand")
    }
}

I use the following generic function to convert objects and arrays to JSON:

func toJSON<T : Encodable>(_ object: T) -> String?
{
    do
    {
        let jsonEncoder = JSONEncoder()
        let jsonEncode = try jsonEncoder.encode(object)
        return String(data: jsonEncode, encoding: .utf8)
    }
    catch
    {
        return nil
    }
}

Now let's say I want to convert the following variable to JSON:

var arrayOfCars: Array<Car> = []
arrayOfCars.append(SUVCar(1700))
arrayOfCars.append(SUVCar(1650))

I use Array<Car> as the type for that array because there are other types of cars in that array. I just made it simpler here for the sake of readability.

So here is what I did:

let json = toJSON(arrayOfCars)

But for some reason, when converting to JSON, the weight attribute of SUVCar is ignored, even though arrayOfCars contains SUVCar objects, and I get a JSON that looks like this:

[{brand: "MyBrand"}, {brand: "MyBrand"}]

So how can I get the weight attribute of SUVCar in my JSON? What did I miss?

Thanks.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
matteoh
  • 2,810
  • 2
  • 29
  • 54
  • You parsing decoding an array of `Car`s why would you expect it to decode an array of `SUVCar`s? Change your array declaration to `var arrayOfCars: [SUVCar] = []` – Leo Dabus Apr 07 '21 at 14:15
  • @LeoDabus Well, because it works like a charm in Kotlin, so I was expecting a similar behavior – matteoh Apr 07 '21 at 14:20

2 Answers2

3
class SUVCar: Car
{
    enum SUVCarKeys: CodingKey {
        case weight
    }
    var weight: Int

    init(_ weight: Int)
    {
        self.weight = weight
        super.init(brand: "MyBrand")
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: SUVCarKeys.self)
        try container.encode(weight, forKey: .weight)
        try super.encode(to: encoder)
    }
}

If you augment the subclass's implementation of encode then you can add in additional properties

Shadowrun
  • 3,572
  • 1
  • 15
  • 13
  • Thanks, I'll try that. But I'm amazed by the amount of code I need to add, while in Kotlin and using the Gson library, I can do exactly the same in one single line of code... – matteoh Apr 07 '21 at 14:28
1

Instead of customising all your subclasses to encode properly you could solve this by introducing a type that hold your different kind of cars

struct CarCollection: Encodable {
    let suvs: [SUVCar]
    let jeeps: [Jeep]
    let sedans: [Sedan]
}

(assuming two other sublclasses class Jeep: Car {} and class Sedan: Car {})

Then you would need no extra code and encoding would be simple

let cars = CarCollection(suvs: [SUVCar(brand: "x", weight: 1000)],
                         jeeps: [Jeep(brand: "y")],
                         sedans: [Sedan(brand: "z")])

if let json = toJSON(cars) {
    print(json)
}

{"jeeps":[{"brand":"x"}],"suvs":[{"brand":"x"}],"sedans":[{"brand":"a"}]}


A bit off topic but struct might be a better choice than class here or at least it would be the recommended choice so here is how it would look with struct and a protocol instead of the superclass. The code above would still be the same.

protocol Car : Encodable {
    var brand: String { get set }
}

struct SUVCar : Car {
    var brand: String
    var weight: Int
}

struct Jeep: Car {
    var brand: String
}
struct Sedan: Car {
    var brand: String
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52