-1

I'm trying to intialize an array of Items from a json file. To this end, I followed Apple's tutorial with re: to doing it (The algorithm is in data.swift but I'll post an abridged version down as well) My issue is that the API I'm pulling data from serves up decimals in quotation marks leading me to get the error

typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "average_cost", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))

What Apple's json decoder expects:

[{
        "company": "Bioseed",
        "item_class": "Seeds",
        "name": "9909",
        "stock": 0,
        "average_cost": 0.00,  // Doubles without quotation marks
        "otc_price": 0.00,
        "dealer_price": 0.00,
        "ctc_price": 0.00
}]

Sample data from my API saved in items.json:

[{
        "company": "Bioseed",
        "item_class": "Seeds",
        "name": "9909",
        "stock": 0,
        "average_cost": "0.00",
        "otc_price": "0.00",
        "dealer_price": "0.00",
        "ctc_price": "0.00"
}]

I could probably rewrite my API to serve decimals and ints without quotation marks however it's already being used by other applications so I would rather not risk breaking something.

So is there a way to tell the decoded to ignore the quotation marks?

Item struct:

struct Item : Decodable {

    var company: String
    var item_class: String
    var name: String
    var stock: Int
    var average_cost: Decimal
    var otc_price: Decimal
    var dealer_price: Decimal
    var ctc_price: Decimal

Loading function:

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

Calling it:

    let items: [Item] = load("items.json")
    print(items)
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
Lorenzo Ang
  • 1,202
  • 3
  • 12
  • 35
  • 1
    Anything that's enclosed in `"` is treated as a String by `Codable`. If you want to convert these values to Ints or Decimals, you'll have to write that conversion code yourself, by overriding the `init(from:)` method. – Gereon Jun 18 '19 at 14:37
  • 1
    Possible duplicate of [Swift 4 decoding doubles from JSON](https://stackoverflow.com/questions/52119001/swift-4-decoding-doubles-from-json). Another question that might be of interest, https://stackoverflow.com/questions/47935705/using-codable-with-key-that-is-sometimes-an-int-and-other-times-a-string – Joakim Danielson Jun 18 '19 at 14:44

1 Answers1

0

Here's how I would implement this:

struct Item : Decodable {
    let company: String
    let itemClass: String
    let name: String
    let stock: Int
    let averageCost: Decimal
    let otcPrice: Decimal
    let dealerPrice: Decimal
    let ctcPrice: Decimal

    enum CodingKeys: String, CodingKey {
        case company
        case itemClass = "item_class"
        case name
        case stock
        case averageCost = "average_cost"
        case otcPrice = "otc_price"
        case dealerPrice = "dealer_price"
        case ctcPrice = "ctc_price"
    }

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

        self.company = try container.decode(String.self, forKey: .company)
        self.itemClass = try container.decode(String.self, forKey: .itemClass)
        self.name = try container.decode(String.self, forKey: .name)
        self.stock = try container.decode(Int.self, forKey: .stock)

        guard
            let averageCost = Decimal(string: try container.decode(String.self, forKey: .averageCost))
        else {
            throw DecodingError.dataCorruptedError(forKey: .averageCost, in: container, debugDescription: "not a Decimal.")
        }

        guard
            let otcPrice = Decimal(string: try container.decode(String.self, forKey: .otcPrice))
        else {
            throw DecodingError.dataCorruptedError(forKey: .otcPrice, in: container, debugDescription: "not a Decimal.")
        }

        guard
            let dealerPrice = Decimal(string: try container.decode(String.self, forKey: .dealerPrice))
        else {
            throw DecodingError.dataCorruptedError(forKey: .dealerPrice, in: container, debugDescription: "not a Decimal.")
        }

        guard
            let ctcPrice = Decimal(string: try container.decode(String.self, forKey: .ctcPrice))
        else {
            throw DecodingError.dataCorruptedError(forKey: .ctcPrice, in: container, debugDescription: "not a Decimal.")
        }


        self.averageCost = averageCost
        self.otcPrice = otcPrice
        self.dealerPrice = dealerPrice
        self.ctcPrice = ctcPrice
    }
}

Alternatively, you could keep the String properties in your model, and convert to decimals on access

struct Item : Decodable {
    let company: String
    let itemClass: String
    let name: String
    let stock: Int
    let _averageCost: String
    let _otcPrice: String
    let _dealerPrice: String
    let _ctcPrice: String

    enum CodingKeys: String, CodingKey {
        case company
        case itemClass = "item_class"
        case name
        case stock
        case _averageCost = "average_cost"
        case _otcPrice = "otc_price"
        case _dealerPrice = "dealer_price"
        case _ctcPrice = "ctc_price"
    }

    var averageCost: Decimal {
        return Decimal(string: _averageCost) ?? .zero
    }

    // ... and so on for the other properties
}
Gereon
  • 17,258
  • 4
  • 42
  • 73