Как преобразовать массив из нескольких типов объектов в JSON, не пропуская ни одного атрибута объекта в Swift?

#json #swift #encodable

#json #swift #кодируемый

Вопрос:

В моем проекте iOS Swift мне нужно преобразовать объекты в JSON.

У меня есть один простой класс:

 class Car : Encodable
{
    var brand: String

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

и один подкласс:

 class SUVCar : Car
{
    var weight: Int

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

Я использую следующую общую функцию для преобразования объектов и массивов в 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
    }
}
 

Теперь предположим, что я хочу преобразовать следующую переменную в JSON:

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

Я использую Array<Car> в качестве типа для этого массива, потому что в этом массиве есть другие типы автомобилей. Я просто упростил его здесь для удобства чтения.

Итак, вот что я сделал:

 let json = toJSON(arrayOfCars)
 

Но по какой-то причине при преобразовании в JSON weight атрибут of SUVCar игнорируется, даже если arrayOfCars содержит SUVCar объекты, и я получаю JSON, который выглядит следующим образом:

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

Итак, как я могу получить weight атрибут SUVCar в моем JSON? Что я пропустил?

Спасибо.

Комментарии:

1. Вы анализируете декодирование массива Car s, почему вы ожидаете, что он будет декодировать массив SUVCar s? Измените объявление массива на var arrayOfCars: [SUVCar] = []

2. @LeoDabus Ну, потому что в Kotlin это работает как шарм, поэтому я ожидал подобного поведения

Ответ №1:

 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)
    }
}
 

Если вы увеличите реализацию encode для подкласса, вы сможете добавить дополнительные свойства

Комментарии:

1. Спасибо, я попробую это. Но я поражен количеством кода, который мне нужно добавить, в то время как в Kotlin и используя библиотеку Gson, я могу сделать то же самое в одной единственной строке кода…

Ответ №2:

Вместо того, чтобы настраивать все ваши подклассы для правильного кодирования, вы могли бы решить эту проблему, введя тип, который содержит ваши разные типы автомобилей

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

(предполагая два других подкласса class Jeep: Car {} и class Sedan: Car {} )

Тогда вам не понадобится дополнительный код, и кодирование будет простым

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

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

{«джипы»: [{«бренд»:»x»}], «внедорожники»: [{«бренд»:»x»}], «седаны»: [{«бренд»: «a»}]}


Немного не по теме, но struct может быть лучшим выбором, чем class здесь, или, по крайней мере, это был бы рекомендуемый выбор, поэтому вот как это будет выглядеть со struct и протоколом вместо суперкласса. Приведенный выше код останется тем же.

 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
}