juhamaja
juhamaja

Reputation: 58

Parsing Decimal from JSON presented as string

With Xcode 10.2 and iOS 12.x we were able to extract Decimal from json string. With Xcode 11.1 and iOS 13.1 it is throwing exception

Expected to decode Double but found a string/data instead.

class MyClass : Codable {

     var decimal: Decimal?
 }

then trying to parse it

let json = "{\"decimal\":\"0.007\"}"
let data = json.data(using: .utf8)
let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "s1", negativeInfinity: "s2", nan: "s3")
 do {
   let t = try decoder.decode(MyClass.self, from: data!)
 } catch {
   print(error)
 }

If I change json string as

let json = "{\"decimal\":0.007}"

It works, but then again we are losing precision. Any ideas?

Upvotes: 3

Views: 2980

Answers (6)

Reza Dehnavi
Reza Dehnavi

Reputation: 2273

I just add the following code. It supports optional as well.

extension KeyedDecodingContainer {

    func decode(_ type: Decimal.Type, forKey key: K) throws -> Decimal {
        let stringValue = try decode(String.self, forKey: key)
        guard let decimalValue = Decimal(string: stringValue) else {
            let context = DecodingError.Context(codingPath: [key], debugDescription: "The key \(key) couldn't be converted to a Decimal value")
            throw DecodingError.typeMismatch(type, context)
        }
        return decimalValue
    }

    func decodeIfPresent(_ type: Decimal.Type, forKey key: K) throws -> Decimal? {
        guard 
            let stringValue = try? decodeIfPresent(String.self, forKey: key),
            let decimalValue = Decimal(string: stringValue)
        else { return nil }
        return decimalValue
    }
}

Upvotes: 3

bandejapaisa
bandejapaisa

Reputation: 26952

You would need to extend KeyedDecodingContainer and add an implementation for Decimal.Type.

extension KeyedDecodingContainer {
    func decode(_ type: Decimal.Type, forKey key: K) throws -> Decimal {
        let stringValue = try decode(String.self, forKey: key)
        guard let decimalValue = Decimal(string: stringValue) else {
            let context = DecodingError.Context(codingPath: [key], debugDescription: "The key \(key) couldn't be converted to a Decimal value")
            throw DecodingError.typeMismatch(type, context)
        }
        return decimalValue
    }
}

Here is an example:

let json = """
{
  "capAmount": "123.45"
}
"""

struct Status: Decodable {
    let capAmount: Decimal

    enum CodingKeys: String, CodingKey {
        case capAmount
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        capAmount = try container.decode(Decimal.self, forKey: .capAmount)
    }
}

// Execute it
if let data = json.data(using: .utf8){
    let status = try JSONDecoder().decode(Status.self, from: data)
    print(status.capAmount)
}

Upvotes: 8

The type should be Double and define also in the parsing as Double. Swift will figure out the rest

struct MyClass: Decodable {
        let decimal: Double

        //can be renamed to follow the API name.
        enum CodingKeys: String, CodingKey {
            case decimal
        }
    }
    extension MyClass {
        init(from decoder: Decoder) throws {

            let values = try decoder.container(keyedBy: CodingKeys.self)
            decimal = try values.decode(Double.self, forKey: .decimal)

        }
    }

Upvotes: -1

Leo Dabus
Leo Dabus

Reputation: 236260

struct Root: Codable {
    let decimal: Decimal
}

extension Root {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        decimal = try Decimal(string: container.decode(String.self, forKey: .decimal)) ?? .zero
    }
}

let json = #"{"decimal":"0.007"}"# 
do {
    let root = try JSONDecoder().decode(Root.self, from: .init(json.utf8))
    print(root)
} catch {
    print(error)
}

This will print

Root(decimal: 0.007)

Upvotes: 3

Joakim Danielson
Joakim Danielson

Reputation: 51831

That decoding strategy has nothing to do with numbers being represented as strings. What you need to do is to implement init(from:) and convert from string there

class MyClass : Codable {
    var decimal: Double?

    enum CodingKeys: String, CodingKey {
        case decimal = "test"
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        decimal = Double(try container.decode(String.self, forKey: .decimal)
        //or if Decimal is used:
        //decimal = Decimal(string: try container.decode(String.self, forKey: .decimal)
    }
}

Note that I am using Double instead of Decimal here to make it simpler

Upvotes: 1

Alessio Campanelli
Alessio Campanelli

Reputation: 1020

I believe that a cleaner solution is declare value not like a string but like a value:

"test": 0.007

having a struct like that:

struct Stuff {
     var test: Decimal
}

and then:

let decoder = JSONDecoder()
let stuff = try decoder.decode(Stuff.self, from: json)

otherwise you can use this example:

https://forums.swift.org/t/parsing-decimal-values-from-json/6906/3

Upvotes: -1

Related Questions