Codable struct 如何解析 any 类型的 JSON
in 技术 with 4 comments

Codable struct 如何解析 any 类型的 JSON

in 技术 with 4 comments

前言

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 类型。当你试着去解码 StringInt 类型的时候会得到正确的值,这个解决方法是非常有效的。

但是,解码 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。

引用

Responses
  1. Steven

    给个大拇指点赞!

    Reply
  2. huaqi
    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")))

    在map(JSONValue.string))这里,会收获一个错误 'JSONValue' is not a subtype of 'String', ,难道只有我的Xcode才会爆这个错误?
    Xcode 版本 10.1 ,Swift 版本: 4

    Reply
    1. @huaqi

      我创建了项目:https://github.com/mzying2013/TestCodableStruct,测试了代码。结果是正确的。如果你任然有问题,可以提供一下你的实例代码。

      Reply
  3. ted

    struct NetModel: Codable {

    var code: Int var data: JSONValue var msg: String

    }

    博主,问一个小问题:我定义了一个这样的model,data的类型可能是String、Array、Dictionary。。
    你这种方法确实很方便的能将返回的值转化成JSONValue。但是JSONValue要怎么再转化成String、Array...这些类型啊?

    Reply