前言
Swift 4 带来了很多很酷的东西,像 Codable 协议。它让解析 JSON 变得简单和容易。但是,在新的 Codable 协议中,并不会像之前的API一样简单。
Codable = Decodable & Encodable,swift 中的定义:
/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable
问题
我正在定义 JSON 的模型,而模型中有一个字段是 Any 类型。
"type": {}
在老的 JSONSerialization
API 中,你可以在 struct 中申明一个 Any 类型的属性,并且从 JSON 字典中通过检索来给它赋值。
Codable 并不像这样简单。假如你申明这个属性为 Any:
let stringExample = """
{"anyProperty": "aString"}
"""
let intExample = """
{"anyProperty": 1}
"""
struct MyStruct: Decodable {
let anyProperty: Any
}
try JSONDecoder().decode(MyStruct.self, from: stringExample.data(using: .utf8)!)
""" 语法用于表示长文本的 String。
你会看到如下的错误:
cannot automatically synthesize 'Decodable' because 'Any' does not conform to 'Decodable'
简单的解决办法
一个简单的解决办法就是自定义解码:
struct MyStruct: Decodable {
let anyProperty: Any
enum CodingKeys: String, CodingKey {
case anyProperty
}
init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringProperty = try? container.decode(String.self, forKey: .anyProperty) {
anyProperty = stringProperty
} else if let intProperty = try? container.decode(Int.self, forKey: .anyProperty) {
anyProperty = intProperty
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON"))
}
}
}
}
详情请参考 Apple 官方文档:https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
首先,在 Decodable
协议的 init
方法里,试着去解码数据为 String
类型。如果失败,则解码为 Int
类型。当你试着去解码 String
和 Int
类型的时候会得到正确的值,这个解决方法是非常有效的。
但是,解码 JSON 对象并非如此的简单。因为你并不知道它的类型,你唯一选择的是去试着解码它作为 [String : Any]
。但是 [String : Any]
是不可解码,因为 Any
不可解码。你可以迭代这个 key 和 value,然后循环去利用这个解决办法做相同的事情,但它会无穷无尽,并永不终止。
正确的解决办法
首先你需要去定义一个 JSON 结构体:
enum JSONValue {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
}
然后,让这个结构体实现 Decodable
协议:
public enum JSONValue: Decodable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode(Int.self) {
self = .int(value)
} else if let value = try? container.decode(Double.self) {
self = .double(value)
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode([String: JSONValue].self) {
self = .object(value)
} else if let value = try? container.decode([JSONValue].self) {
self = .array(value)
} else {
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON"))
}
}
现在 JSONValue
现在可以被解码了,[String : JSONValue]
和 [JSONValue]
可以直接开箱即用。你的结构体不需要去做任何自定义解码工作。
struct MyStruct: Decodable {
let anyProperty: JSONValue
}
JSONValue
是一个非常有用的枚举。不需要去引用任何的 Any
属性。你可以把当前属性类型转换为解包这个枚举的关联值。
Nil
上面的解决办法任然不是非常完美。你可以利用 nil
合运算符(nil coalescing operator
)和 Optional map
函数来优化它。
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let value =
(try? container.decode(String.self)).map(JSONValue.string) ??
(try? container.decode(Int.self)).map(JSONValue.int) ??
(try? container.decode(Double.self)).map(JSONValue.double) ??
(try? container.decode(Bool.self)).map(JSONValue.bool) ??
(try? container.decode([String: JSONValue].self)).map(JSONValue.object) ??
(try? container.decode([JSONValue].self)).map(JSONValue.array) else {
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON"))
}
self = value
}
这样看起来更好,但是它并不起多大作用。它只会让你的 MacBook 风扇发疯。编译器不喜欢 nil
的合运算符。有个解决这个问题的妙招是去创建一个 Optional
的 extension ,像下面这样:
extension Optional {
func or(_ other: Optional) -> Optional {
switch self {
case .none: return other
case .some: return self
}
}
}
现在,你可以把 ??
替换为 or
:
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let value = ((try? container.decode(String.self)).map(JSONValue.string))
.or((try? container.decode(Int.self)).map(JSONValue.int))
.or((try? container.decode(Double.self)).map(JSONValue.double))
.or((try? container.decode(Bool.self)).map(JSONValue.bool))
.or((try? container.decode([String: JSONValue].self)).map(JSONValue.object))
.or((try? container.decode([JSONValue].self)).map(JSONValue.array))
else {
throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON"))
}
self = value
}
最后,你可以再给 Optional 添加一个 extension 方法,用于处理 error 的情况:
extension Optional {
func resolve(with error: @autoclosure () -> Error) throws -> Wrapped {
switch self {
case .none: throw error()
case .some(let wrapped): return wrapped
}
}
}
这可以让我们的解决方案看起来几乎只有一行代码:
public enum JSONValue: Decodable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = try ((try? container.decode(String.self)).map(JSONValue.string))
.or((try? container.decode(Int.self)).map(JSONValue.int))
.or((try? container.decode(Double.self)).map(JSONValue.double))
.or((try? container.decode(Bool.self)).map(JSONValue.bool))
.or((try? container.decode([String: JSONValue].self)).map(JSONValue.object))
.or((try? container.decode([JSONValue].self)).map(JSONValue.array))
.resolve(with: DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON")))
}
}
结语
现在,你拥有了一个优雅的 struct,能够用于去解码任何类型的 JSON 对象。
let jsondoc = """
{
"imAString": "aString",
"imAnInt": 1,
"imADouble": 0.5,
"imABool": true,
"imAnArray": [1,2,"3",false],
"imAnArrayOfArrays": [[[[[[[true]]]]]]],
"imAnObject": {"imAnotherString": "anotherString"},
"imAnObjectInAnObject": {"anObj": {"anInt": 1}},
"imAnArrayOfObjects": [{"anObj": {"anInt": 1}}, {"aBool": true}]
}
"""
let parsed: JSONValue = try JSONDecoder().decode(JSONValue.self, from: jsondoc.data(using: .utf8)!)
它就像用 JSONSerialization
解析 [String: Any],但是我们的方案更加类型安全一点。
JSONValue
解决了我的问题,它应该也会解决你的。欢迎评论,让我知道你的想法。在后续的文章中,我会继续更新它,让它支持 Encodable
,这将给我们提供一个调制解调器一样的方式去使用 JSON。
引用
- 原英文引用:Parsing fields in Codable structs that can be of any JSON type
- nil coalescing operator
- Optional map
本文由 Bill 创作。
最后编辑时间为: 2019.01.21 at 10:55 am
给个大拇指点赞!
在map(JSONValue.string))这里,会收获一个错误 'JSONValue' is not a subtype of 'String', ,难道只有我的Xcode才会爆这个错误?
Xcode 版本 10.1 ,Swift 版本: 4
我创建了项目:https://github.com/mzying2013/TestCodableStruct,测试了代码。结果是正确的。如果你任然有问题,可以提供一下你的实例代码。
struct NetModel: Codable {
var code: Int var data: JSONValue var msg: String}
博主,问一个小问题:我定义了一个这样的model,data的类型可能是String、Array、Dictionary。。
你这种方法确实很方便的能将返回的值转化成JSONValue。但是JSONValue要怎么再转化成String、Array...这些类型啊?
请看我文章最后几句话,"在后续的文章中,我会继续更新它,让它支持 Encodable"。如果你需要的话,我可以写一个 Demo 给你。
想了解一下 encoder那部分的代码 想学习学习 希望更新
支持Encodable了吗