Swift 4 JSON Decodable 解码类型更改的最简单方法
Posted
技术标签:
【中文标题】Swift 4 JSON Decodable 解码类型更改的最简单方法【英文标题】:Swift 4 JSON Decodable simplest way to decode type change 【发布时间】:2017-11-19 13:12:13 【问题描述】:借助 Swift 4 的 Codable 协议,可以实现高水平的底层日期和数据转换策略。
鉴于 JSON:
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
我想把它强制成下面的结构
struct ExampleJson: Decodable
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey
case name, age
case taxRate = "tax_rate"
日期解码策略可以将基于字符串的日期转换为日期。
有什么东西可以用基于字符串的浮点数来做吗
否则我一直坚持使用 CodingKey 引入字符串并使用计算 get:
enum CodingKeys: String, CodingKey
case name, age
case sTaxRate = "tax_rate"
var sTaxRate: String
var taxRate: Float return Float(sTaxRate) ?? 0.0
这种方式让我做的维护工作比看起来应该需要的要多。
这是最简单的方式还是有类似于 DateDecodingStrategy 的其他类型转换?
更新:我应该注意:我也走了重写路线
init(from decoder:Decoder)
但这是相反的方向,因为它迫使我为自己做这一切。
【问题讨论】:
谢谢@Rob,我用这个疏忽解决了这个问题。 我遇到了同样的问题并打开了一个 !Swift bug。在 JSON 中将数字包装为字符串是如此普遍,我希望 Swift 团队能够处理这种情况。 看来 Swift 团队正在研究这个问题。手指交叉! 请参阅my answer,其中显示了最多 3 种不同的方法来解决您的问题。 【参考方案1】:不幸的是,我认为当前的JSONDecoder
API 中不存在这样的选项。仅存在一个选项以将convert exceptional floating-point values 与字符串表示进行往来。
另一种可能的手动解码解决方案是为任何LosslessStringConvertible
定义一个Codable
包装器类型,该包装器类型可以对其String
表示进行编码和解码:
struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable
var decoded: Decoded
init(_ decoded: Decoded)
self.decoded = decoded
init(from decoder: Decoder) throws
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
guard let decoded = Decoded(decodedString) else
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
self.decoded = decoded
func encode(to encoder: Encoder) throws
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
然后你可以只拥有这种类型的属性并使用自动生成的Codable
一致性:
struct Example : Codable
var name: String
var age: Int
var taxRate: StringCodableMap<Float>
private enum CodingKeys: String, CodingKey
case name, age
case taxRate = "tax_rate"
尽管不幸的是,现在您必须使用 taxRate.decoded
来与 Float
值进行交互。
但是,您始终可以定义一个简单的转发计算属性来缓解这种情况:
struct Example : Codable
var name: String
var age: Int
private var _taxRate: StringCodableMap<Float>
var taxRate: Float
get return _taxRate.decoded
set _taxRate.decoded = newValue
private enum CodingKeys: String, CodingKey
case name, age
case _taxRate = "tax_rate"
尽管这还没有达到应有的水平——希望JSONDecoder
API 的更高版本将包含更多自定义解码选项,或者能够在Codable
API 中表达类型转换自己。
但是,创建包装器类型的一个优点是它也可以用于简化手动解码和编码。例如,手动解码:
struct Example : Decodable
var name: String
var age: Int
var taxRate: Float
private enum CodingKeys: String, CodingKey
case name, age
case taxRate = "tax_rate"
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
【讨论】:
那么这会成为一个 Swift 提案吗? @LordAndrei 我建议在swift evolution mailing list 上提出它。我最初的感觉是,最好将它作为JSONDecoder
/JSONEncoder
的额外选项,而不是作为Codable
的大修。鉴于现有的将异常浮点值解码和编码为字符串的选项,它似乎是一个很自然的地方。【参考方案2】:
使用 Swift 5.1,您可以选择以下三种方式之一来解决您的问题。
#1。使用Decodable
init(from:)
初始化器
当您需要将单个结构、枚举或类从String
转换为Float
时,请使用此策略。
import Foundation
struct ExampleJson: Decodable
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey
case name, age, taxRate = "tax_rate"
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
age = try container.decode(Int.self, forKey: CodingKeys.age)
let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
guard let taxRateFloat = Float(taxRateString) else
let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
taxRate = taxRateFloat
用法:
import Foundation
let jsonString = """
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#2。使用中间模型
当您的 JSON 中有许多嵌套键或需要从 JSON 转换许多键(例如,从 String
到 Float
)时,请使用此策略。
import Foundation
fileprivate struct PrivateExampleJson: Decodable
var name: String
var age: Int
var taxRate: String
enum CodingKeys: String, CodingKey
case name, age, taxRate = "tax_rate"
struct ExampleJson: Decodable
var name: String
var age: Int
var taxRate: Float
init(from decoder: Decoder) throws
let privateExampleJson = try PrivateExampleJson(from: decoder)
name = privateExampleJson.name
age = privateExampleJson.age
guard let convertedTaxRate = Float(privateExampleJson.taxRate) else
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
taxRate = convertedTaxRate
用法:
import Foundation
let jsonString = """
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#3。使用KeyedDecodingContainer
扩展方法
从某些 JSON 键的类型转换为模型的属性类型(例如,String
到 Float
)时使用此策略是您应用程序中的常见模式。
import Foundation
extension KeyedDecodingContainer
func decode(_ type: Float.Type, forKey key: Key) throws -> Float
if let stringValue = try? self.decode(String.self, forKey: key)
guard let floatValue = Float(stringValue) else
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
return floatValue
else
let doubleValue = try self.decode(Double.self, forKey: key)
return Float(doubleValue)
struct ExampleJson: Decodable
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey
case name, age, taxRate = "tax_rate"
用法:
import Foundation
let jsonString = """
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
【讨论】:
KeyedDecodingContainer
选项很好,只要您的 所有 浮点数都表示为字符串。如果 JSON 包含一个不带引号的浮点数,则会出现解码错误,因为 KeyedDecodingContainer
将期待一个字符串。
@TomHarrington 完全正确。我稍后会更新我的答案以解决此问题。谢谢。
第一个选项只有在将枚举从结构声明中取出后才对我有用。谢谢!【参考方案3】:
您始终可以手动解码。所以,给定:
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
你可以这样做:
struct Example: Codable
let name: String
let age: Int
let taxRate: Float
init(from decoder: Decoder) throws
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
age = try values.decode(Int.self, forKey: .age)
guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else
throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
taxRate = rate
enum CodingKeys: String, CodingKey
case name, age
case taxRate = "tax_rate"
请参阅Encoding and Decoding Custom Types 中的手动编码和解码。
但我同意,考虑到有多少 JSON 源错误地将数值返回为字符串,似乎应该有一个更优雅的字符串转换过程,相当于 DateDecodingStrategy
。
【讨论】:
感谢您的回复。我已经编辑了我的原始查询,我走了这条路;但这与我的目标相反。对于仍在学习这个新 API 的人来说,这是一个很好的信息。【参考方案4】:我知道这是一个很晚的答案,但我几天前才开始研究Codable
。我遇到了类似的问题。
为了将字符串转换为浮点数,可以给KeyedDecodingContainer
写一个扩展,并从init(from decoder: Decoder)
调用扩展中的方法
本期提到的问题,见我下面写的扩展;
extension KeyedDecodingContainer
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float?
guard let value = try decodeIfPresent(transformFrom, forKey: key) else
return nil
return Float(value)
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float
guard let valueAsString = try? decode(transformFrom, forKey: key),
let value = Float(valueAsString) else
throw DecodingError.typeMismatch(
type,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Decoding of \(type) from \(transformFrom) failed"
)
)
return value
您可以从init(from decoder: Decoder)
方法调用此方法。请参阅下面的示例;
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
实际上,您可以使用这种方法将任何类型的数据转换为任何其他类型。您可以转换string to Date
、string to bool
、string to float
、float to int
等。
实际上要将字符串转换为日期对象,我更喜欢这种方法而不是JSONEncoder().dateEncodingStrategy
,因为如果你写得正确,你可以在同一个响应中包含不同的日期格式。
希望我能帮上忙。
更新了解码方法以返回来自@Neil 的建议的非可选方法。
【讨论】:
我发现这是最优雅的解决方案。但是,decode()
版本不应返回可选项。我将发布非可选版本作为新答案。【参考方案5】:
我使用了 Suran 的版本,但对其进行了更新以返回 decode() 的非可选值。对我来说,这是最优雅的版本。斯威夫特 5.2。
extension KeyedDecodingContainer
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float?
guard let value = try decodeIfPresent(transformFrom, forKey: key) else
return nil
return Float(value)
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float
guard let str = try? decode(transformFrom, forKey: key),
let value = Float(str) else
throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
return value
【讨论】:
这看起来不错。这将如何用于编码和解码?我可以创建一堆与 String 绑定的类型别名(HexA、HexB、HexC 等)以强制将不同类型的转换为 Int 吗?我有一个关于我的用例的更多详细信息的问题:***.com/questions/65314663/…【参考方案6】:您可以使用lazy var
将属性转换为另一种类型:
struct ExampleJson: Decodable
var name: String
var age: Int
lazy var taxRate: Float =
Float(self.tax_rate)!
()
private var tax_rate: String
这种方法的一个缺点是如果你想访问taxRate
,你不能定义一个let
常量,因为你第一次访问它时,你正在改变结构。
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
【讨论】:
对我来说是最好的解决方案,极简主义?【参考方案7】:上面的选项只处理给定字段总是字符串的情况。很多时候,我遇到过输出曾经是字符串,其他时候是数字的 API。所以这是我解决这个问题的建议。您可以更改它以引发异常或将解码值设置为 nil。
var json = """
"title": "Apple",
"id": "20"
""";
var jsonWithInt = """
"title": "Apple",
"id": 20
""";
struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable
var value: T
init(from decoder: Decoder)
print("Decoding")
if let container = try? decoder.singleValueContainer()
if let val = try? container.decode(T.self)
value = val
return
if let str = try? container.decode(String.self)
value = T.init(str) ?? T.zero
return
value = T.zero
struct MyData: Decodable
let title: String
let _id: DecodableNumberFromStringToo<Int>
enum CodingKeys: String, CodingKey
case title, _id = "id"
var id: Int
return _id.value
do
let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)
print(parsedJson.id)
catch
print(error as? DecodingError)
do
let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)
print(parsedJson.id)
catch
print(error as? DecodingError)
【讨论】:
谢谢谢谢。这个功能应该内置在解码器中(尽管不要问我为什么服务器有时会在引号中加上数字,有时不会)。【参考方案8】:如何在 Swift 4 中使用 JSONDecodable:
-
获取 JSON 响应并创建结构
在 Struct 中符合 Decodable 类
this GitHub project中的其他步骤,一个简单的例子
【讨论】:
以上是关于Swift 4 JSON Decodable 解码类型更改的最简单方法的主要内容,如果未能解决你的问题,请参考以下文章
Swift Decodable - 如何解码经过 base64 编码的嵌套 JSON
使用 Swift 4 Decodable 将字符串 JSON 响应转换为布尔值
Swift 4 Decodable - 没有与键 CodingKeys 关联的值 [重复]