Лучший подход к декодированию одного и того же JSON с другой структурой в Swift?

Я имел дело с запросом на изменение структуры нашей модели iOS, модификация заключалась в перемещении параметра EncodingOption в класс EditingOptions из OutputOptions struct.

Иерархия конфигурации

AppConfig {
profile: ProfileSetting
}

ProfileSetting {
editSetting:EditingOptions
outputSetting: OutputOptions
}

Предыдущая структура данных:

class EditingOptions: NSObject, Codable {
//..other parameters
}

struct OutputOptions: Codable {
var barcodeEncoding: EncodingOption = .utf8
//..other parameters
}

Измененная структура данных:

class EditSetting: NSObject, Codable {
var encodingOption: EncodingOption
//..other parameters
}

struct OutputSetting: Codable {
//..other parameters
}

Измененная структура была несовместима с предыдущим файлом конфигурации с ошибкой JSON keyNotFound:

keyNotFound(CodingKeys(stringValue: "encodingOption", intValue: nil), 
Swift.DecodingError.Context(codingPath: [
CodingKeys(stringValue: "profileManage", intValue: nil), CodingKeys(stringValue: "deviceName",
 intValue: nil), CodingKeys(stringValue: "profiles", intValue: nil), 
_JSONKey(stringValue: "Index 0", intValue: 0), 
CodingKeys(stringValue: "outputOptions", intValue: nil)
],
 debugDescription: "No value associated with key CodingKeys(stringValue: \"encodingOption\", intValue: nil) (\"encodingOption\").", underlyingError: nil))

Решением ChatGPT было добавление метода init(from decoder: Decoder) для класса ProfileSetting:

required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        //...other parameters
        
        if let editingOptions = try? container.decode(EditingOptions.self, forKey: .editingOptions) {
            self.editingOptions = editingOptions
        } else {
            self.editingOptions = EditingOptions()
        }
    }

barcodeEncoding был скрытым параметром с настройкой по умолчанию и еще не был опубликован.

Решение устранило ошибку keyNotFound, однако я не уверен, что это решение было ПРАВИЛЬНЫМ способом решения проблемы.

Непонятно: изменился ли JSON или нужно изменить только вашу модель iOS? Или JSON использует две версии, в которых указан параметр кодирования (в зависимости от вызова)?

Larme 29.07.2024 10:33

Извините за недопонимание, изменилась только модель iOS, и она должна быть совместима с предыдущим форматом JSON (который до изменения поставлялся с моделью iOS).

ChengEn 29.07.2024 10:37

Что вы подразумеваете под «правильным» способом? Ваш код работает так, как вы ожидаете? Если да, то это правильно.

Sweeper 29.07.2024 10:40

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

Joakim Danielson 29.07.2024 10:40

Я не понимаю, почему вы исправляете это реальное решение. Если encodingOption нет в JSON (а есть пользовательское значение), вы можете просто сделать enum CodingKeys: String, CodingKey { case abc } и поместить только ключи, присутствующие в JSON, опуская encodingOption.

Larme 29.07.2024 11:32

@JoakimDanielson Я тестирую несколько случаев, способ «исправления» приведет к минусам, заключающимся в том, что все параметры редактирования оговорок будут сброшены до значений по умолчанию, если editingOptions равен нулю. Однако, если не использовать дополнительный метод try, все работает как по маслу, но я не уверен, как это работает. self.editingOptions = try container.decode(EditingOptions.self, forKey: .editingOptions)

ChengEn 29.07.2024 11:39

@Larme, encodingOption уже был в JSON до модификации со значением по умолчанию.

ChengEn 29.07.2024 11:42

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

ChengEn 29.07.2024 11:48

Я не проверял, но, по моему мнению, не всегда ли вы попадаете в self.editingOptions = EditingOptions() случай?

Larme 29.07.2024 12:18

Используйте app.quicktype.io. Дайте ему все доступные случаи, и он сгенерирует для вас парсер.

Cy-4AH 29.07.2024 17:55
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
1
10
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если я правильно понял, вашу проблему можно упростить так:

 let json = """
 {
    "a": { "keyA": "Value for KeyA" },
    "b": { "keyB": "Value for KeyB",
            "otherKey": "My value To Pass to a now "}
 }
"""

Где otherKey твой encodingOption. Он присутствует на B, но теперь вы хотите передать его A.

Итак, старый код:

func oldCode(with json: String) {
    struct OldParent: Codable {
        let a: OldA
        let b: OldB
    }
    
    class OldA: NSObject, Codable {
        let keyA: String
    }
    
    struct OldB: Codable {
        let keyB: String
        let otherKey: String
    }
    
    let decoder = JSONDecoder()
    
    do {
        let parent = try decoder.decode(OldParent.self, from: Data(json.utf8))
        print(parent)
        print("--")
    } catch {
        print("Error: \(error)")
    }
}

Он работает так, как задумано, ваша модель iOS отражает модель API.

Теперь вы хотите изменить свою модель iOS, но модель API не изменится.

Возможное решение — сохранить otherKey в B, а внутри декодирования Parent (которое знает и A, и B) передать это значение в A:

func newCode(with json: String) {
    struct NewParent: Codable {
        let a: NewA
        let b: NewB
        
        init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.a = try container.decode(NewA.self, forKey: .a)
            self.b = try container.decode(NewB.self, forKey: .b)
            self.a.otherKey = self.b.otherKey //Pass the value
        }
    }
    
    class NewA: NSObject, Codable {
        let keyA: String
        var otherKey: String = "defaultValue"
        
        enum CodingKeys: CodingKey {
            case keyA //We skip here otherKey to tell the decoder to decode only keyA which is present in the JSON
        }
    }
    
    struct NewB: Codable {
        let keyB: String
        let otherKey: String //Only because it's in JSON
    }
    
    let decoder = JSONDecoder()
    
    do {
        let parent = try decoder.decode(NewParent.self, from: Data(json.utf8))
        print(parent)
        print("--")
    } catch {
        print("Error: \(error)")
    }
}

Обновлено: добавление еще одной альтернативы:

Если вам не нравится наличие let otherKey: String //Only because it's in JSON в B и вы хотите, чтобы он был только в A, вы действительно можете расшифровать его самостоятельно. Я не проводил тест на эффективность, но думаю, это займет немного больше времени, потому что нужно снова декодировать контейнер для B (один раз для otherKey и другой для B, может быть кеш и другие оптимизации, я полностью не проверял).

func newCode2(with json: String) {
    struct NewParent: Codable {
        let a: NewA
        let b: NewB
        
        init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.a = try container.decode(NewA.self, forKey: .a)
            self.b = try container.decode(NewB.self, forKey: .b)
            // Here decode again b and look for the value of otherKey
            let containerForOtherKeyInB = try container.nestedContainer(keyedBy: CustomKey.self, forKey: .b)
            self.a.otherKey = try containerForOtherKeyInB.decode(String.self, forKey: .otherKey)
        }
    }
    
    class NewA: NSObject, Codable {
        let keyA: String
        var otherKey: String = "defaultValue"
        
        enum CodingKeys: CodingKey {
            case keyA //We skip here otherKey
        }
    }
    enum CustomKey: CodingKey {
        case otherKey
    }
    struct NewB: Codable {
        let keyB: String
    }
    
    let decoder = JSONDecoder()
    
    do {
        let parent = try decoder.decode(NewParent.self, from: Data(json.utf8))
        print(parent)
        print("--")
    } catch {
        print("Error: \(error)")
    }
}

Ваше понимание было правильным! Завтра проверю ваше решение, спасибо за подробное описание!

ChengEn 29.07.2024 15:24

Другие вопросы по теме