Декодируемый и JSON, 2 типа данных для одной и той же переменной

avatar
Nicolai Harbo
8 апреля 2018 в 11:58
513
2
3

Я использую протокол Decodable для декодирования некоторого json, но столкнулся с проблемой:

Я получаю ответ, где долгота и широта могут быть как целыми числами (широта = 0), если к элементу не добавлены данные о географическом местоположении, так и строкой (fx. latitude = "25.047880") если есть геоданные. Теперь, когда я декодирую json, я не знаю, как построить свой Struct, так как long и lat не могут быть одновременно String и Int. Поэтому я получаю ошибку декодирования при выборке элементов, где представлены оба случая.

Есть предложения по решению этой проблемы? Я пытался использовать «Любой» в качестве типа данных, но это не соответствует протоколу декодирования

.
struct JPhoto: Decodable {
  let id: String
  let farm: Int
  let secret: String
  let server: String
  let owner: String
  let title: String
  let latitude: String //Can both be Int and String
  let longitude: String //Can both be Int and String
}
Источник
vadian
8 апреля 2018 в 12:42
0

Вы должны написать собственный инициализатор для обработки случаев. Прочтите Кодирование и декодирование пользовательских типов

Ответы (2)

avatar
sketchyTech
8 апреля 2018 в 13:05
6

Вам нужно написать собственный кодировщик/декодер. Для этого вы можете использовать связанное перечисление значений, используя оператор switch для кодирования и поведение бросания/отлова для декодирования:

enum AngularDistance:Codable {
    case string(String), integer(Int)

    func encode(to encoder: Encoder) throws {
        switch self {
        case .string(let str):
            var container = encoder.singleValueContainer()
            try container.encode(str)
        case .integer(let int):
            var container = encoder.singleValueContainer()
            try container.encode(int)
        }
    }

    init(from decoder: Decoder) throws {
        do {
            let container = try decoder.singleValueContainer()
            let str = try container.decode(String.self)
            self = AngularDistance.string(str)
        }
        catch {
              do { let container = try decoder.singleValueContainer()
                   let int = try container.decode(Int.self)
                   self = AngularDistance.integer(int) 
              }
              catch {
                   throw DecodingError.typeMismatch(AngularDistance.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected to decode an Int or a String"))
              }
        }
    }
}

Вот пример кодирования и декодирования этого типа AngularDistance:

let lat = [AngularDistance.string("String"), AngularDistance.integer(10)]
let encoder = JSONEncoder()
var decoder = JSONDecoder()

do {
    let encoded = try encoder.encode(lat)
    try decoder.decode(Array<AngularDistance>.self, from: encoded)
}
catch DecodingError.typeMismatch(let t, let e)  {
    t
    e.codingPath
    e.debugDescription
}
catch {
    print(error.localizedDescription)
    }

А вот ваша структура переписана:

struct JPhoto: Decodable {
  let id: String
  let farm: Int
  let secret: String
  let server: String
  let owner: String
  let title: String
  let latitude: AngularDistance //Can both be Int and String
  let longitude: AngularDistance //Can both be Int and String
}
Nicolai Harbo
8 апреля 2018 в 13:17
0

Спасибо за ваш комментарий! Это имеет смысл, большая часть этого! Что меня немного смущает, так это то, нужен ли мне средний фрагмент кода? Например, мне просто нужно добавить перечисление, а затем переписать структуру, верно? :-)

nayem
8 апреля 2018 в 13:58
1

Кстати, вы не обрабатывали случаи ошибок. Что делать, если возвращаемое значение не является String или Int? Вы должны обрабатывать эти сценарии, явно бросая DecodingError.

sketchyTech
8 апреля 2018 в 16:39
0

@nayem Как и для всех других свойств в структуре Codable, если значение не существует или если оно имеет другой тип, при попытке экземпляра JSONDecoder декодировать будет выдана ошибка. Codable просто не будет анализировать неправильно отформатированную строку и выдает ошибку «Данные не могут быть прочитаны, потому что они имеют неправильный формат».

sketchyTech
8 апреля 2018 в 16:45
0

@NicolaiHarbo да, вы просто переписываете структуру, но при обработке полученных значений нужно помнить, что вам нужно извлечь связанные значения.

nayem
8 апреля 2018 в 17:27
0

Хотя это правда то, что вы сказали. Но это не объяснительное описание, не так ли? Это довольно расплывчато.

sketchyTech
8 апреля 2018 в 18:33
0

Вы можете добавить обработку ошибок, если хотите, но я бы не считал это необходимым, если только вы не собираетесь тестировать каждый тип одинаково. Так что да, если вы добавляете обработку ошибок в String, Int, Bool, Dictionary и Array, это может быть полезно для отладки. Но просто изолировать один тип и сказать, что мы должны добавить обработку ошибок, само по себе не будет особенно полезным.

nayem
8 апреля 2018 в 18:42
0

Почему бы нет? Хорошо, давайте попробуем расшифровать Bool и скажите мне, какую ошибку вы получаете? Тогда я покажу вам, как это улучшить.

sketchyTech
8 апреля 2018 в 19:22
1

@nayem Итак, есть перечисление DecodingError, о котором я не знал. Спасибо, что побудили меня исследовать это. На основе этого я добавил в свой пример элементарную поддержку ошибок.

nayem
9 апреля 2018 в 02:46
1

Да! Вот и все. Я указывал вам на это. Рад, что ты это понял.

avatar
Paul B
4 ноября 2021 в 12:04
-1

В дополнение к перечислениям со связанными значениями следует упомянуть еще несколько подходов. Вы можете использовать предложенную структуру Either<Int, String> или IntOrString для ваших latitude и longitude.

struct Either<F: Codable, S: Codable>: Codable {
    var firstValue: F?
    var secondValue: S?

    var value: Any? {
        return firstValue ?? secondValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if firstValue != nil {
            try? container.encode(firstValue)
        } else {
            try? container.encode(secondValue)
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        firstValue = try? container.decode(F.self)
        secondValue = try? container.decode(S.self)
        if firstValue == nil && secondValue == nil {
            //Type mismatch
            throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(F.self) and also not \(S.self)"))
        }
    }
}

Еще один способ сделать то же самое:

struct IntOrString: Codable {
    
    var value: Any
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if  let intValue = value as? Int {
            try? container.encode(intValue)
        } else if let strValue = value as? String {
            try? container.encode(strValue)
        }
    }
    
    init(from decoder: Decoder) throws {
        if let int = try? Int(from: decoder) {
            value = int
            return
        }
        value = try String(from: decoder)
    }
}