如何处理完全动态的JSON响应

rlu*_*bke 7 json swift swift4

也许社区中的某些人有类似的困难,并提出了一个可行的解决方案.

我们目前正在开发一个多语言键/值存储.鉴于此,我们通常不知道提前存储的内容.

考虑以下结构

struct Character : Codable, Equatable {
    let name:    String
    let age:     Int
    let gender:  Gender
    let hobbies: [String]

    static func ==(lhs: Character, rhs: Character) -> Bool {
        return (lhs.name == rhs.name
                   && lhs.age == rhs.age
                   && lhs.gender == rhs.gender
                   && lhs.hobbies == rhs.hobbies)
    }
}
Run Code Online (Sandbox Code Playgroud)

通过线路发送/接收字符实体时,一切都非常简单.用户可以向我们提供我们可以解码的类型.

但是,我们确实能够动态查询存储在后端中的实体.例如,我们可以请求'name'属性的值并返回该值.

这种活力是一个痛点.除了不知道它们是Codable之外的属性类型之外,返回的格式也可以是动态的.

以下是两个不同调用提取属性的响应示例:

{"value":"Bilbo"}
Run Code Online (Sandbox Code Playgroud)

{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
Run Code Online (Sandbox Code Playgroud)

在某些情况下,它可能相当于字典.

现在,我有以下结构来处理响应:

fileprivate struct ScalarValue<T: Decodable> : Decodable {
    var value: T?
}
Run Code Online (Sandbox Code Playgroud)

使用Character示例,传递给解码器的类型将是:

ScalarValue<Character>.self
Run Code Online (Sandbox Code Playgroud)

但是,对于单值,数组或字典情况,我有点卡住了.

我从一开始就开始:

fileprivate struct AnyDecodable: Decodable {
    init(from decoder: Decoder) throws {
        // ???
    }
}
Run Code Online (Sandbox Code Playgroud)

根据我上面描述的可能的返回类型,我不确定这是否可以使用当前的API.

思考?

Rob*_*ier 7

Swift绝对可以处理任意JSON可解码.这与任意可解码不同.JSON无法编码所有可能的值.但是这个结构将解码任何可以用JSON表达的东西,从那里你可以以类型安全的方式探索它,而不需要使用危险和笨拙的工具Any.

enum JSON: Decodable, CustomStringConvertible {
    var description: String {
        switch self {
        case .string(let string): return "\"\(string)\""
        case .number(let double):
            if let int = Int(exactly: double) {
                return "\(int)"
            } else {
                return "\(double)"
            }
        case .object(let object):
            return "\(object)"
        case .array(let array):
            return "\(array)"
        case .bool(let bool):
            return "\(bool)"
        case .null:
            return "null"
        }
    }

    var isEmpty: Bool {
        switch self {
        case .string(let string): return string.isEmpty
        case .object(let object): return object.isEmpty
        case .array(let array): return array.isEmpty
        case .null: return true
        case .number, .bool: return false
        }
    }

    struct Key: CodingKey, Hashable, CustomStringConvertible {
        var description: String {
            return stringValue
        }

        var hashValue: Int { return stringValue.hash }

        static func ==(lhs: JSON.Key, rhs: JSON.Key) -> Bool {
            return lhs.stringValue == rhs.stringValue
        }

        let stringValue: String
        init(_ string: String) { self.stringValue = string }
        init?(stringValue: String) { self.init(stringValue) }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    case string(String)
    case number(Double) // FIXME: Split Int and Double
    case object([Key: JSON])
    case array([JSON])
    case bool(Bool)
    case null

    init(from decoder: Decoder) throws {
        if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
        else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
        else if let object = try? decoder.container(keyedBy: Key.self) {
            var result: [Key: JSON] = [:]
            for key in object.allKeys {
                result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
            }
            self = .object(result)
        }
        else if var array = try? decoder.unkeyedContainer() {
            var result: [JSON] = []
            for _ in 0..<(array.count ?? 0) {
                result.append(try array.decode(JSON.self))
            }
            self = .array(result)
        }
        else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
        else {
            self = .null
        }
    }

    var objectValue: [String: JSON]? {
        switch self {
        case .object(let object):
            let mapped: [String: JSON] = Dictionary(uniqueKeysWithValues:
                object.map { (key, value) in (key.stringValue, value) })
            return mapped
        default: return nil
        }
    }

    var arrayValue: [JSON]? {
        switch self {
        case .array(let array): return array
        default: return nil
        }
    }

    subscript(key: String) -> JSON? {
        guard let jsonKey = Key(stringValue: key),
            case .object(let object) = self,
            let value = object[jsonKey]
            else { return nil }
        return value
    }

    var stringValue: String? {
        switch self {
        case .string(let string): return string
        default: return nil
        }
    }

    var doubleValue: Double? {
        switch self {
        case .number(let number): return number
        default: return nil
        }
    }

    var intValue: Int? {
        switch self {
        case .number(let number): return Int(number)
        default: return nil
        }
    }

    subscript(index: Int) -> JSON? {
        switch self {
        case .array(let array): return array[index]
        default: return nil
        }
    }

    var boolValue: Bool? {
        switch self {
        case .bool(let bool): return bool
        default: return nil
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这个,你可以做以下事情:

let bilboJSON = """
{"value":"Bilbo"}
""".data(using: .utf8)!

let bilbo = try! JSONDecoder().decode(JSON.self, from: bilboJSON)
bilbo["value"]  // "Bilbo"

let javaJSON = """
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
""".data(using: .utf8)!

let java = try! JSONDecoder().decode(JSON.self, from: javaJSON)
java["value"]?[1]   // ["Bilbo", 111]
java["value"]?[1]?[0]?.stringValue  // "Bilbo" (as a String rather than a JSON.string)
Run Code Online (Sandbox Code Playgroud)

扩散?有点难看,但throws在我的实验中使用并不能真正使界面更好(特别是因为下标不能抛出).根据您的特定用例,可能需要进行一些调整.