What Is Preventing My Conversion from String to Int When Decoding Using Swift 4's Codable

What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?

The issue you're seeing has to do with a combination of how your init(with: Decoder) is written, and how DataDecoder type decodes its type argument. Since the test is failing with nil != Optional(5), then either uvIndex or currentWeatherReport is nil in currentWeatherReport?.uvIndex.

Let's see how uvIndex might be nil. Since it's an Int?, it gets a default value of nil if not otherwise initialized, so that's a good place to start looking. How might it get assigned its default value?

internal init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let uvIndexInteger = try container.decodeIfPresent(Int.self, forKey: .uvIndex) {
// Clearly assigned to here
uvIndex = uvIndexInteger
} else if let uvIndexString = try container.decodeIfPresent(String.self, forKey: .uvIndex) {
// Clearly assigned to here
uvIndex = Int(uvIndexString)
}

// Hmm, what happens if neither condition is true?
}

Hmm. So, if decoding as both an Int and a String fail (because the value isn't present), you'll get nil. But clearly, this isn't always the case, since the first test passes (and a value is indeed there).

So, on to the next failure mode: if an Int is clearly being decoded, why is the String not decoding properly? Well, when uvIndex is a String, the following decode call is still made:

try container.decodeIfPresent(Int.self, forKey: .uvIndex)

That call only returns nil if a value isn't present (i.e. there's no value for the given key, or the value is explicitly null); if a value is present but is not an Int, the call will throw.

Since the error being thrown isn't caught and explicitly handled, it propagates up immediately, never calling try container.decodeIfPresent(String.self, forKey: .uvIndex). Instead, the error bubbles up to where CurrentWeatherReport is decoded:

internal final class func decode(_ data: Data) -> T? {
return try? JSONDecoder().decode(T.self, from: data)
}

Since this code try?s, the error gets swallowed up, returning nil. That nil makes its way to the original currentWeatherReport?.uvIndex call, which ends up being nil not because uvIndex was missing, but because the whole report failed to decode.

Likely, the init(with: Decoder) implementation that fits your needs is more along the lines of the following:

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

// try? container.decode(...) returns nil if the value was the wrong type or was missing.
// You can also opt to try container.decode(...) and catch the error.
if let uvIndexInteger = try? container.decode(Int.self, forKey: .uvIndex) {
uvIndex = uvIndexInteger
} else if let uvIndexString = try? container.decode(String.self, forKey: .uvIndex) {
uvIndex = Int(uvIndexString)
} else {
// Not strictly necessary, but might be clearer.
uvIndex = nil
}
}

Using Codable to encode/decode from Strings to Ints with a function in between

For Codable encoding and decoding that requires custom transformation type stuff, like that, you just need to implement the initializer and encode methods yourself. In your case it would look something like this. It's a bit verbose just to try to get the idea across really clearly.

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let hexAString: String = try container.decode(String.self, forKey: .hexA)
self.hexA = hexAConversion(from: hexAString)
let hexBString: String = try container.decode(String.self, forKey: .hexB)
self.hexB = hexBConversion(from: hexBString)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
//Assuming you have another set of methods for converting back to a String for encoding
try container.encode(self.name, forKey: .name)
try container.encode(hexAStringConversion(from: self.hexA), forKey: .hexA)
try container.encode(hexBStringConversion(from: self.hexB), forKey: .hexB)
}

Swift 4 Codable - API provides sometimes an Int sometimes a String

you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "rating" is an Int or String, sign all required class properties at last.

here is a simple demo i made:

class Demo: Decodable {
var test = 0
var rating: String?

enum CodingKeys: String, CodingKey {
case test
case rating
}

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let test = try container.decode(Int.self, forKey: .test)
let ratingString = try? container.decode(String.self, forKey: .rating)
let ratingInt = try? container.decode(Int.self, forKey: .rating)
self.rating = ratingString ?? (ratingInt == 0 ? "rating is nil or 0" : "rating is integer but not 0")
self.test = test
}
}

let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: YOUR-JSON-DATA)
  • if rating API's value is normal string, you will get it as you wish.
  • if rating API's value is 0, rating will equal to "rating is nil or 0"
  • if rating API's value is other integers, rating will be "rating is integer but not 0"

you may modify decoded "rating" result, that should be easy.

hope this could give you a little help. :)

for more info: Apple's encoding and decoding doc

How to write the codable in generic format

You can do that using the following model:

struct ResponseDataModel<T: Codable>: Codable{
let data : DataClass<T>?
enum CodingKeys: String, CodingKey{
case data = "data"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass<T>.self, forKey:.data)
}
}

struct DataClass<T: Codable>: Codable {
let id: T?
let name: String?
let age: Int?

enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(T.self, forKey:.id)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}

However, you should always known the type of the id property when you call decode(_:from:) function of JSONDecoder like this:

let decoder = JSONDecoder()

do {
let decoded = try decoder.decode(ResponseDataModel<Int>.self, from: data)
print(decoded)
} catch {
print(error)
}

Or you can use the following model to always map the id as Int, even if your server sends it as String:

struct ResponseDataModel: Codable{
let data : DataClass?
enum CodingKeys: String, CodingKey{
case data = "data"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}

struct DataClass: Codable {
let id: Int?
let name: String?
let age: Int?

enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try values.decodeIfPresent(Int.self, forKey:.id)
} catch DecodingError.typeMismatch {
if let idString = try values.decodeIfPresent(String.self, forKey:.id) {
id = Int(idString)
} else {
id = nil
}
}
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}

Type conversion with Swift 4's Codable

For each Resolution, you want to decode a single string, and then parse that into two Int components. To decode a single value, you want to get a singleValueContainer() from the decoder in your implementation of init(from:), and then call .decode(String.self) on it.

You can then use components(separatedBy:) in order to get the components, and then Int's string initialiser to convert those to integers, throwing a DecodingError.dataCorruptedError if you run into an incorrectly formatted string.

Encoding is simpler, as you can just use string interpolation in order to encode a string into a single value container.

For example:

import Foundation

struct Resolution {
let width: Int
let height: Int
}

extension Resolution : Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

let resolutionString = try container.decode(String.self)
let resolutionComponents = resolutionString.components(separatedBy: "x")

guard resolutionComponents.count == 2,
let width = Int(resolutionComponents[0]),
let height = Int(resolutionComponents[1])
else {
throw DecodingError.dataCorruptedError(in: container, debugDescription:
"""
Incorrectly formatted resolution string "\(resolutionString)". \
It must be in the form <width>x<height>, where width and height are \
representable as Ints
"""
)
}

self.width = width
self.height = height
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("\(width)x\(height)")
}
}

You can then use it like so:

struct Image : Codable {

let imageID: Int
let resolutions: [Resolution]

private enum CodingKeys : String, CodingKey {
case imageID = "image_id", resolutions
}
}

let jsonData = """
{
"image_id": 1,
"resolutions": ["1920x1200", "1920x1080"]
}
""".data(using: .utf8)!

do {
let image = try JSONDecoder().decode(Image.self, from: jsonData)
print(image)
} catch {
print(error)
}

// Image(imageID: 1, resolutions: [
// Resolution(width: 1920, height: 1200),
// Resolution(width: 1920, height: 1080)
// ]
// )

Note we've defined a custom nested CodingKeys type in Image so we can have a camelCase property name for imageID, but specify that the JSON object key is image_id.

parse JSON with swift via a model to object - decode int/string trouble

This wasn't the problem that the error message gave!

All I needed to do to fix the problem was to employ CodingKeys.

I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.

Now an example of my model:

struct Product
{
let name: String
let long_name_value: String

...

    enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"

...

    }
}

I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

Using codable with value that is sometimes an Int and other times a String

struct GeneralProduct: Codable {
var price: Double?
var id: String?
var name: String?
private enum CodingKeys: String, CodingKey {
case price = "p", id = "i", name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
price = try container.decode(Double.self, forKey: .price)
name = try container.decode(String.self, forKey: .name)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}


let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""

let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""


do {
let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
print(product.price ?? "nil")
print(product.id ?? "nil")
print(product.name ?? "nil")
} catch {
print(error)
}

edit/update:

You can also simply assign nil to your id when your api returns 0:

do {
let value = try container.decode(Int.self, forKey: .id)
id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}

Swift 4 Codable: Converting JSON return String to Int/Date/Float

This is not yet possible as Swift team has provided only String to date decoder in JSONDecoder.

You can always decode manually though:

struct WaitTimeContainer: Decodable {
let waitTimes: [WaitTime]

private enum CodingKeys: String, CodingKey {
case waitTimes = "WaitTimes"
}

struct WaitTime:Decodable {
let checkpointIndex: Int
let waitTime: Float
let createdDateTime: Date

init(checkpointIndex: Int, waitTime: Float, createdDateTime:Date) {
self.checkpointIndex = checkpointIndex
self.waitTime = waitTime
self.createdDateTime = createdDateTime
}

static let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "MM/dd/yyyy hh:mm:ss a"
return formatter
}()

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let checkpointIndexString = try container.decode(String.self, forKey: .checkpointIndex)
let checkpointIndex = Int(checkpointIndexString)!

let waitTimeString = try container.decode(String.self, forKey: .waitTime)
let waitTime = Float(waitTimeString)!

let createdDateTimeString = try container.decode(String.self, forKey: .createdDateTime)

let createdDateTime = WaitTime.formatter.date(from: createdDateTimeString)!

self.init(checkpointIndex:checkpointIndex, waitTime:waitTime, createdDateTime:createdDateTime)
}

private enum CodingKeys: String, CodingKey {
case checkpointIndex = "CheckpointIndex"
case waitTime = "WaitTime"
case createdDateTime = "Created_Datetime"
}
}
}

Convert string to Date/Int/Double using codable

struct SimpleOrder: Codable {
var created: Date?
var orderId: String?
var price: String?
private enum CodingKeys: String, CodingKey {
case created = "time", orderId = "id", price
}
init(created: Date? = nil, orderId: String? = nil, price: String? = nil) {
self.created = created
self.orderId = orderId
self.price = price
}
}

extension SimpleOrder {
var priceValue: Double? {
guard let price = price else { return nil }
return Double(price)
}
}

extension Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
}

Decoding the json data returned by the API:

let jsonData = Data("""
{
"time": "2017-12-01T20:41:48.700Z",
"id": "0001",
"price": "9.99"
}
""".utf8)

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(Formatter.iso8601)

do {
let simpleOrder = try decoder.decode(SimpleOrder.self, from: jsonData)
print(simpleOrder)
} catch {
print(error)
}

Or initialising a new object with no values:

var order = SimpleOrder()

Problem while converting JSON to Swift The data couldn’t be read because it isn’t in the correct format.

First of all

never print(error.localizedDescription).

in a JSONDecoder catch block. You get a generic but quite meaningless error message.

Always print the entire error, DecodingErrors are very descriptive

print(error)

Your code contains three major errors, one of them (error #3) occurs multiple times

Error #1

typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))

indicates that the root object is an array, the JSON starts clearly with [

Solution: Decode [CryptListStruct].self


Error #2

dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "volume_1mth_usd", intValue: nil)], debugDescription: "Parsed JSON number <3699822674922524.74> does not fit in Int.", underlyingError: nil))

indicates that the received value 3699822674922524.74 is actually a Double.

Solution: Declare

let volume1MthUsd: Double

Error #3

keyNotFound(CodingKeys(stringValue: "id_icon", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 4", intValue: 4)], debugDescription: "No value associated with key CodingKeys(stringValue: "id_icon", intValue: nil) ("id_icon").", underlyingError: nil))

indicates that the key id_icon is missing (at least) in the 5th item of the array.

Solution: Declare the type as optional

let idIcon : String?

The same error occurs for dataTradeStart, dataTradeEnd, dataQuoteStart, dataQuoteEnd, dataOrderbookStart, dataOrderbookEnd, dataStart, dataEnd

let dataQuoteStart, dataQuoteEnd, dataOrderbookStart, dataOrderbookEnd: String?
let dataTradeStart, dataTradeEnd : String?
let dataStart, dataEnd: String?

Side note:

You can delete the entire CodingKeys enum if you replace assetID with assetId and add the convertFromSnakeCase key decoding strategy

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let crpytList = try decoder.decode([CryptListStruct].self, from: data)


Related Topics



Leave a reply



Submit