Как проанализировать JSON с декодируемым протоколом, когда типы свойств могут измениться с Int на String? - PullRequest
0 голосов
/ 03 октября 2018

Мне нужно декодировать JSON с большой структурой и множеством вложенных массивов.Я воспроизвел структуру в моем файле UserModel, и она работает, за исключением одного свойства (почтовый индекс), которое находится во вложенном массиве (Location), который иногда является Int, а другой - String.Я не знаю, как справиться с этой ситуацией, и пробовал много разных решений.Последнее, что я попробовал, это из этого блога https://agostini.tech/2017/11/12/swift-4-codable-in-real-life-part-2/ И он предлагает использовать дженерики.Но теперь я не могу инициализировать объект Location без предоставления Decoder ():

enter image description here

Любая помощь или любой другой подход приветствуются.Это вызов API: https://api.randomuser.me/?results=100&seed=xmoba Это мой файл UserModel:

import Foundation
import UIKit
import ObjectMapper

struct PostModel: Equatable, Decodable{

    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        if lhs.userId != rhs.userId {
            return false
        }
        if lhs.id != rhs.id {
            return false
        }
        if lhs.title != rhs.title {
            return false
        }
        if lhs.body != rhs.body {
            return false
        }
        return true
    }


    var userId : Int
    var id : Int
    var title : String
    var body : String

    enum key : CodingKey {
        case userId
        case id
        case title
        case body
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: key.self)
        let userId = try container.decode(Int.self, forKey: .userId)
        let id = try container.decode(Int.self, forKey: .id)
        let title = try container.decode(String.self, forKey: .title)
        let body = try container.decode(String.self, forKey: .body)

        self.init(userId: userId, id: id, title: title, body: body)
    }

    init(userId : Int, id : Int, title : String, body : String) {
        self.userId = userId
        self.id = id
        self.title = title
        self.body = body
    }
    init?(map: Map){
        self.id = 0
        self.title = ""
        self.body = ""
        self.userId = 0
    }
}

extension PostModel: Mappable {



    mutating func mapping(map: Map) {
        id       <- map["id"]
        title     <- map["title"]
        body     <- map["body"]
        userId     <- map["userId"]
    }

}

Ответы [ 3 ]

0 голосов
/ 03 октября 2018

Вы можете использовать generic следующим образом:

enum Either<L, R> {
    case left(L)
    case right(R)
}

extension Either: Decodable where L: Decodable, R: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let left = try? container.decode(L.self) {
            self = .left(left)
        } else if let right = try? container.decode(R.self) {
            self = .right(right)
        } else {
            throw DecodingError.typeMismatch(Either<L, R>.self, .init(codingPath: decoder.codingPath, debugDescription: "Expected either `\(L.self)` or `\(R.self)`"))
        }
    }
}

extension Either: Encodable where L: Encodable, R: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case let .left(left):
            try container.encode(left)
        case let .right(right):
            try container.encode(right)
        }
    }
}

И затем объявить postcode: Either<Int, String>, и если ваша модель Decodable, а все остальные поля Decodable, то дополнительный код не потребуется.

0 голосов
/ 03 октября 2018

Ну, это обычная проблема IntOrString.Вы можете просто сделать тип вашей собственности enum, который может обрабатывать либо String, либо Int.

enum IntOrString: Codable {
    case int(Int)
    case string(String)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .int(container.decode(Int.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .string(container.decode(String.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)"))
            }
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let int):
            try container.encode(int)
        case .string(let string):
            try container.encode(string)
        }
    }
}

Поскольку я обнаружил несоответствие вашей модели, которую вы разместили в своем вопросе, и вконечная точка API, на которую вы указали, я создал свою собственную модель и собственный JSON, который необходимо декодировать.

struct PostModel: Decodable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
    let postCode: IntOrString
    // you don't need to implement init(from decoder: Decoder) throws
    // because all the properties are already Decodable
}

Декодирование, когда postCode равно Int:

let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": 9999
}
""".data(using: .utf8)!
do {
    let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
    if case .int(let int) = postModel.postCode {
        print(int) // prints 9999
    } else if case .string(let string) = postModel.postCode {
        print(string)
    }
} catch {
    print(error)
}

Декодирование, когда postCode равно String:

let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": "9999"
}
""".data(using: .utf8)!
do {
    let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
    if case .int(let int) = postModel.postCode {
        print(int)
    } else if case .string(let string) = postModel.postCode {
        print(string) // prints "9999"
    }
} catch {
    print(error)
}
0 голосов
/ 03 октября 2018

Если postcode может быть как String, так и Int, у вас есть (как минимум) два возможных решения этой проблемы.Во-первых, вы можете просто сохранить все почтовые индексы как String, так как все Int могут быть преобразованы в String.Это кажется лучшим решением, поскольку маловероятно, что вам понадобится выполнять какие-либо числовые операции с почтовым индексом, особенно если некоторые почтовые индексы могут быть String.Другим решением будет создание двух свойств для почтового индекса, одного типа String? и одного типа Int? и всегда заполнение только одного из двух в зависимости от входных данных, как объяснено в Использование кодирования с ключом, который являетсяиногда Int и иногда String .

Решение, сохраняющее все почтовые индексы как String:

struct PostModel: Equatable, Decodable {
    static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
        return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && lhs.body == rhs.body
    }

    var userId: Int
    var id: Int
    var title: String
    var body: String
    var postcode: String

    enum CodingKeys: String, CodingKey {
        case userId, id, title, body, postcode
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.userId = try container.decode(Int.self, forKey: .userId)
        self.id = try container.decode(Int.self, forKey: .id)
        self.title = try container.decode(String.self, forKey: .title)
        self.body = try container.decode(String.self, forKey: .body)
        if let postcode = try? container.decode(String.self, forKey: .postcode) {
            self.postcode = postcode
        } else {
            let numericPostcode = try container.decode(Int.self, forKey: .postcode)
            self.postcode = "\(numericPostcode)"
        }
    }
}
...