【问题标题】:Parsing Decimal from JSON presented as string从呈现为字符串的 JSON 解析十进制
【发布时间】:2023-04-03 19:14:01
【问题描述】:

使用 Xcode 10.2 和 iOS 12.x,我们能够从 json 字符串中提取 Decimal。对于 Xcode 11.1 和 iOS 13.1,它会抛出异常

应解码 Double,但找到了一个字符串/数据。

class MyClass : Codable {

     var decimal: Decimal?
 }

然后尝试解析它

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)
 }

如果我将 json 字符串更改为

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

它有效,但我们又一次失去了精确度。有什么想法吗?

【问题讨论】:

  • *.com/a/55131900/2303865的可能重复
  • 您是从服务器接收字符串还是双精度数?
  • 我都收到了,但现在字符串是有问题的。将双精度解析为十进制仍然可以。

标签: swift ios13 codable


【解决方案1】:

您需要扩展 KeyedDecodingContainer 并为 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
    }
}

这是一个例子:

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)
}

【讨论】:

【解决方案2】:
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)
}

这将打印出来

根(十进制:0.007)

【讨论】:

  • 这越来越近了。现在,如果我有这样的模型' class SomeModel : Codable { var title: String var enabled: Bool var amount : Root // used to be Decimal before etc } ' 而且我在几个对象中需要 Decimal 是很常见的接收。我必须实现很多过去自动工作的代码
  • 我认为最简单的方法是将它们视为字符串。您可以使用计算属性返回十进制值
  • 是的,我实际上最终编写了 JSONDecoder 的修改版本,它也接受字符串。这仍然是实际问题的最佳答案,因此将此广告标记为已解决
【解决方案3】:

该解码策略与将数字表示为字符串无关。您需要做的是实现init(from:) 并从那里转换字符串

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)
    }
}

请注意,我在这里使用 Double 而不是 Decimal 以使其更简单

【讨论】:

    【解决方案4】:

    我相信一个更干净的解决方案是声明值不是像一个字符串而是像一个值:

    "test": 0.007
    

    有这样的结构:

    struct Stuff {
         var test: Decimal
    }
    

    然后:

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

    否则你可以使用这个例子:

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

    【讨论】:

    • 我无法控制后端发送的内容,这以前可以正常工作。示例只是为了澄清问题
    • 好的,你可以试试这个例子:forums.swift.org/t/parsing-decimal-values-from-json/6906/3
    • 在这种情况下,我必须事先指定键。我实际上更喜欢使用变量名作为键。
    【解决方案5】:

    类型应该是 Double 并且在解析中也定义为 Double。 Swift 会解决剩下的问题

    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)
    
            }
        }
    

    【讨论】: