Swift 结构:为单个 属性 处理多种类型
Swift structures: handling multiple types for a single property
我正在使用 Swift 4 并尝试解析一些 JSON 数据,显然在某些情况下,同一键可能具有不同的类型值,例如:
{
"type": 0.0
}
和
{
"type": "12.44591406"
}
我实际上坚持定义我的 struct
因为我不知道如何处理这种情况,因为
struct ItemRaw: Codable {
let parentType: String
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
抛出 "Expected to decode String but found a number instead."
,当然,
struct ItemRaw: Codable {
let parentType: Float
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
相应地抛出 "Expected to decode Float but found a string/data instead."
。
定义我的 struct
时如何处理这种(以及类似的)情况?
一个简单的解决方案是提供 init(from:)
的实现,它尝试将值解码为 String
,如果由于类型错误而失败,则尝试解码为 Double
:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.parentType = try container.decode(String.self, forKey: .parentType)
} catch DecodingError.typeMismatch {
let value = try container.decode(Double.self, forKey: .parentType)
self.parentType = "\(value)"
}
}
我 运行 在尝试 decode/encode Reddit 列表 JSON 响应中的 "edited" 字段时遇到了同样的问题。我创建了一个结构,表示给定键可能存在的动态类型。键可以是布尔值或整数。
{ "edited": false }
{ "edited": 123456 }
如果你只需要能够解码,只需要实现init(from:)。如果您需要双向,则需要实现 encode(to:) 函数。
struct Edited: Codable {
let isEdited: Bool
let editedTime: Int
// Where we determine what type the value is
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Check for a boolean
do {
isEdited = try container.decode(Bool.self)
editedTime = 0
} catch {
// Check for an integer
editedTime = try container.decode(Int.self)
isEdited = true
}
}
// We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try isEdited ? container.encode(editedTime) : container.encode(false)
}
}
在我的 Codable class 中,然后我使用我的结构。
struct Listing: Codable {
let edited: Edited
}
编辑:针对您的场景的更具体的解决方案
我建议在解码时使用 CodingKey 协议和枚举来存储所有属性。当您创建符合 Codable 的内容时,编译器将为您创建一个私有枚举 CodingKeys。这使您可以根据 JSON Object 属性 键决定要做什么。
举个例子,我解码的是JSON:
{"type": "1.234"}
{"type": 1.234}
如果因为只需要双精度值而想从字符串转换为双精度值,只需解码字符串,然后从中创建双精度值。 (这就是 Itai Ferber 正在做的,然后您还必须使用 try decoder.decode(type:forKey:))
解码所有属性
struct JSONObjectCasted: Codable {
let type: Double?
init(from decoder: Decoder) throws {
// Decode all fields and store them
let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
// First check for a Double
do {
type = try container.decode(Double.self, forKey: .type)
} catch {
// The check for a String and then cast it, this will throw if decoding fails
if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
type = typeValue
} else {
// You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
type = nil
}
}
// Perform other decoding for other properties.
}
}
如果您需要将类型与值一起存储,您可以使用符合 Codable 的枚举而不是结构。然后,您可以将 switch 语句与 JSONObjectCustomEnum 的 "type" 属性 一起使用,并根据情况执行操作。
struct JSONObjectCustomEnum: Codable {
let type: DynamicJSONProperty
}
// Where I can represent all the types that the JSON property can be.
enum DynamicJSONProperty: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Decode the double
do {
let doubleVal = try container.decode(Double.self)
self = .double(doubleVal)
} catch DecodingError.typeMismatch {
// Decode the string
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
我必须解码作为字符串给出的 PHP/MySQL/PDO
双精度值,对于这个用例,我必须扩展 KeyedDecodingContainer
,像这样:
extension KeyedDecodingContainer {
func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
do {
let str = try self.decode(String.self, forKey: key)
if let dbl = Double(str) {
return dbl
}
} catch DecodingError.typeMismatch {
return try self.decode(Double.self, forKey: key)
}
let context = DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Wrong Money Value")
throw DecodingError.typeMismatch(Double.self, context)
}
}
用法:
let data = """
{"value":"1.2"}
""".data(using: .utf8)!
struct Test: Decodable {
let value: Double
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
self.value = try decoder.container(keyedBy: CodingKeys.self)
.decode(forKey: CodingKeys.value)
}
}
try JSONDecoder().decode(Test.self, from: data).value
// 输出 Json
{
"software_id": "10",
"name": "Kroll"
},
{
"software_id": 580,
"name": "Synmed"
}
// 可编码结构
struct SoftwareDataModel: Codable {
var softwareId:MyValue?
var name:String?
enum CodingKeys: String, CodingKey{
case softwareId = "software_id"
case name
}
}
MYValue is Codable Struct Which help to to convert your datatype into "String" here I mentions only String and Int datatypes.
enum MyValue: Codable {
case string(String)
var stringValue: String? {
switch self {
case .string(let s):
return s
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(Int.self) {
self = .string("\(x)")
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
}
}
}
// 如何得到software_id ?
let softwareId = Struct_object.softwareId?.stringValue ?? "0"
我正在使用 Swift 4 并尝试解析一些 JSON 数据,显然在某些情况下,同一键可能具有不同的类型值,例如:
{
"type": 0.0
}
和
{
"type": "12.44591406"
}
我实际上坚持定义我的 struct
因为我不知道如何处理这种情况,因为
struct ItemRaw: Codable {
let parentType: String
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
抛出 "Expected to decode String but found a number instead."
,当然,
struct ItemRaw: Codable {
let parentType: Float
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
相应地抛出 "Expected to decode Float but found a string/data instead."
。
定义我的 struct
时如何处理这种(以及类似的)情况?
一个简单的解决方案是提供 init(from:)
的实现,它尝试将值解码为 String
,如果由于类型错误而失败,则尝试解码为 Double
:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.parentType = try container.decode(String.self, forKey: .parentType)
} catch DecodingError.typeMismatch {
let value = try container.decode(Double.self, forKey: .parentType)
self.parentType = "\(value)"
}
}
我 运行 在尝试 decode/encode Reddit 列表 JSON 响应中的 "edited" 字段时遇到了同样的问题。我创建了一个结构,表示给定键可能存在的动态类型。键可以是布尔值或整数。
{ "edited": false }
{ "edited": 123456 }
如果你只需要能够解码,只需要实现init(from:)。如果您需要双向,则需要实现 encode(to:) 函数。
struct Edited: Codable {
let isEdited: Bool
let editedTime: Int
// Where we determine what type the value is
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Check for a boolean
do {
isEdited = try container.decode(Bool.self)
editedTime = 0
} catch {
// Check for an integer
editedTime = try container.decode(Int.self)
isEdited = true
}
}
// We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try isEdited ? container.encode(editedTime) : container.encode(false)
}
}
在我的 Codable class 中,然后我使用我的结构。
struct Listing: Codable {
let edited: Edited
}
编辑:针对您的场景的更具体的解决方案
我建议在解码时使用 CodingKey 协议和枚举来存储所有属性。当您创建符合 Codable 的内容时,编译器将为您创建一个私有枚举 CodingKeys。这使您可以根据 JSON Object 属性 键决定要做什么。
举个例子,我解码的是JSON:
{"type": "1.234"}
{"type": 1.234}
如果因为只需要双精度值而想从字符串转换为双精度值,只需解码字符串,然后从中创建双精度值。 (这就是 Itai Ferber 正在做的,然后您还必须使用 try decoder.decode(type:forKey:))
解码所有属性struct JSONObjectCasted: Codable {
let type: Double?
init(from decoder: Decoder) throws {
// Decode all fields and store them
let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
// First check for a Double
do {
type = try container.decode(Double.self, forKey: .type)
} catch {
// The check for a String and then cast it, this will throw if decoding fails
if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
type = typeValue
} else {
// You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
type = nil
}
}
// Perform other decoding for other properties.
}
}
如果您需要将类型与值一起存储,您可以使用符合 Codable 的枚举而不是结构。然后,您可以将 switch 语句与 JSONObjectCustomEnum 的 "type" 属性 一起使用,并根据情况执行操作。
struct JSONObjectCustomEnum: Codable {
let type: DynamicJSONProperty
}
// Where I can represent all the types that the JSON property can be.
enum DynamicJSONProperty: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Decode the double
do {
let doubleVal = try container.decode(Double.self)
self = .double(doubleVal)
} catch DecodingError.typeMismatch {
// Decode the string
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
我必须解码作为字符串给出的 PHP/MySQL/PDO
双精度值,对于这个用例,我必须扩展 KeyedDecodingContainer
,像这样:
extension KeyedDecodingContainer {
func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
do {
let str = try self.decode(String.self, forKey: key)
if let dbl = Double(str) {
return dbl
}
} catch DecodingError.typeMismatch {
return try self.decode(Double.self, forKey: key)
}
let context = DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Wrong Money Value")
throw DecodingError.typeMismatch(Double.self, context)
}
}
用法:
let data = """
{"value":"1.2"}
""".data(using: .utf8)!
struct Test: Decodable {
let value: Double
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
self.value = try decoder.container(keyedBy: CodingKeys.self)
.decode(forKey: CodingKeys.value)
}
}
try JSONDecoder().decode(Test.self, from: data).value
// 输出 Json
{
"software_id": "10",
"name": "Kroll"
},
{
"software_id": 580,
"name": "Synmed"
}
// 可编码结构
struct SoftwareDataModel: Codable {
var softwareId:MyValue?
var name:String?
enum CodingKeys: String, CodingKey{
case softwareId = "software_id"
case name
}
}
MYValue is Codable Struct Which help to to convert your datatype into "String" here I mentions only String and Int datatypes.
enum MyValue: Codable {
case string(String)
var stringValue: String? {
switch self {
case .string(let s):
return s
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(Int.self) {
self = .string("\(x)")
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
}
}
}
// 如何得到software_id ?
let softwareId = Struct_object.softwareId?.stringValue ?? "0"