问题
Scenario: Data stream that contains an item that has changed from an Int
to String
type causing the JSON parser to crash.
Result: Subscriber 'sink' crashed with data type not matching the original receiving type via JSON parser.
Goal: to convert the Int
values to String
to have a consistent stream for a successful parsing.
Here's a snippet of the data stream that has caused the crash:
...
{
"city": "אלון שבות",
"sickCount": 124,
"actualSick": 15,
"verifiedLast7Days": " 11-14 ",
"testLast7Days": 699,
"patientDiffPopulationForTenThousands": 47
},
{
"city": "סייד (שבט)",
"sickCount": " קטן מ-15 ",
"actualSick": " קטן מ-15 ",
"verifiedLast7Days": " 0 ",
"testLast7Days": 17,
"patientDiffPopulationForTenThousands": 4
},
...
Here's the error via console:
CodingKeys(stringValue: "sickCount", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))
Here's the code:
func getData() {
let str = "https://disease.sh/v3/covid-19/gov/Israel"
let url = URL(string: str)!
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.receive(on: DispatchQueue.main)
.decode(type: IsraelDataElement.self, decoder: JSONDecoder())
remoteDataPublisher
.eraseToAnyPublisher()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("{IsraelModel} Publisher Finished")
case let .failure(anError):
Swift.print("\nIsrael - Received error: #function", anError)
}
}, receiveValue: { someData in
self.add_UUID(origData: someData)
print(someData)
}).store(in: &cancellables)
}
回答1:
I wasn't sure if what I was suggesting in comments wasn't clear, so here's an example of what I had in mind.
If the object you're trying to decode is:
struct IsraelDataElement {
let city: String
let sickCount: String,
let actualSick: String,
let verifiedLast7Days: String,
let testLast7Days: Int,
let patientDiffPopulationForTenThousands: Int
}
then you can manually decode it, converting renegade Int
s to String
s:
extension IsraelDataElement: Decodable {
private enum CodingKeys: CodingKey {
case city, sickCount, actualSick, verifiedLast7Days //... etc
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
do {
self.sickCount = try String(container.decode(Int.self, forKey: .sickCount))
} catch DecodingError.typeMismatch {
self.sickCount try container.decode(String.self, forKey: .sickCount)
}
// and so on for other properties
}
}
Then, no further changes are needed in your Combine chain.
回答2:
Swift strongly typed so it will not coerce the types for you. If you genuinely mean for the types to be heterogenous then idiomatically you should use an enum to represent that this thing is either an Int or String. This is the only way you can actually round trip the data and preserver the type. For instance:
struct Element: Decodable {
let city: String
let sickCount: Either<String, Int>
let actualSick: Either<String, Int>
let verifiedLast7Days: String
let testLast7Days: Int
let patientDiffPopulationForTenThousands: Int
}
enum Either<Left: Decodable, Right: Decodable>: Decodable {
case left(Left)
case right(Right)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Left.self) {
self = .left(x)
} else if let x = try? container.decode(Right.self) {
self = .right(x)
} else {
throw DecodingError.typeMismatch(Self.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for \(String(describing:Self.self))"))
}
}
}
If this is just a case of your backend developers being sloppy then convert the type yourself and you can do so generically so you can easily fix their data problems everywhere in your app:
struct Element: Decodable {
let city: String
let sickCount: Int
let actualSick: Int
enum CodingKeys: CodingKey {
case city, sickCount, actualSick
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
self.sickCount = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .sickCount)
self.actualSick = try container.decodeAndCoerce(to: Int.self, from: String.self, conversion: Int.init(_:), forKey: .actualSick)
}
}
extension KeyedDecodingContainer {
func decodeAndCoerce<Target: Decodable, Source: Decodable>(to: Target.Type, from: Source.Type, conversion: @escaping (Source) -> Target?, forKey key: Key) throws -> Target {
guard let value = (try? decode(Target.self, forKey: key)) ?? ((try? decode(Source.self, forKey: key)).flatMap(conversion)) else {
throw DecodingError.typeMismatch(Target.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Expected \(String(describing: Target.self))"))
}
return value
}
}
Aren't generics fun?
来源:https://stackoverflow.com/questions/65649602/whats-the-proper-syntax-of-mapping-filtering-a-data-stream-element-to-convert-i