Swift Codable:处理优化的 JSON 生成类型不匹配

Swift Codable: dealing with Optimized JSON generating type Mismatch

我正在尝试找出处理发送优化 JSON 的 API 的正确或最佳方法。 如果您还没有遇到 Optimized JSON as a server respons here is a bit of background,假设您对 #color code 的响应为 #FF2233 服务器发送 {"color" : "FF2233"}, 但是如果代码是 223344 然后服务器发送 {"color" : 223344} 删除引号 对于单个对象数组,它发送不带括号的对象 到目前为止,这是我在操场上进行的测试……它有效,但在我看来,这不是解决我要解决的问题的最佳方法。

有没有办法在尝试解码之前检查值的类型,以便我们至少可以尝试正确地转换它??

struct Test : Codable {

    var a : String?

    enum CodingKeys: String, CodingKey {
        case a = "a"
    }
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        do{
            a = try values.decodeIfPresent(String.self, forKey: .a)
        }
        catch{
           a = String(describing: try values.decodeIfPresent(Int.self, forKey: .a)!)
        }
    }

    init(a: String)
    {
        self.a = a
    }
}

var a = Test(a: "FF2233")
var jd = try? JSONEncoder().encode(a)
var jt = String(bytes: jd!, encoding: .utf8)
jt = "{\"a\":223365}"
jd = jt?.data(using: .utf8)
do{
    let res = try JSONDecoder().decode(Test.self, from: jd!)
    print(res.a!)
} catch{
    print(error)
}

严格来说,您在这里处理的是损坏的 JSON。你的方法是必要的。这是愚蠢的,但只是因为有人做出了一个愚蠢的决定,送你破烂才必要的JSON。

您可以稍微清理一下代码,方法是扩展 KeyedDecodingContainer 以将这些更改包含在一个名为 lenientDecode() 的方法中。然后你可以写类似 a = values.lenientDecode(String.self, forKey:.a) 的东西。您仍会执行相同的检查,但将它们放在一个单独的方法中将使重复检查多个字段变得更加容易。

我在尝试处理您的数据类型时发现它很有趣。

让我们先将其分解为基础类型。在最低级别,您有 IntString。然后你有 Single 个对象或 Array。在最高级别,您应该有一个 struct 可以处理您的 Root 对象。基本上你需要两个 enum 来包裹你的 struct。让我们试试:

enum IntOrString: Codable {
    case int(Int)
    case string(String)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .int(container.decode(Int.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .string(container.decode(String.self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)"))
            }
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let int):
            try container.encode(int)
        case .string(let string):
            try container.encode(string)
        }
    }
}

enum SingleOrArray: Codable {
    case single(IntOrString)
    case array([IntOrString])
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = try .single(container.decode(IntOrString.self))
        } catch DecodingError.typeMismatch {
            do {
                self = try .array(container.decode([IntOrString].self))
            } catch DecodingError.typeMismatch {
                throw DecodingError.typeMismatch(SingleOrArray.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type"))
            }
        }
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .single(let single):
            try container.encode(single)
        case .array(let array):
            try container.encode(array)
        }
    }
}

struct Root: Codable {
    let color: SingleOrArray
}

解码过程:

let jsonData = """
{
 "color":["FF2233", "FF2234", "FF2235"]
}
""".data(using: .utf8)!
do {
    let response = try JSONDecoder().decode(Root.self, from: jsonData)
    //print(response)    //["FF2233", "FF2234", "FF2235"]

    // If you want to get underlying elements from this structure, you might do something like below
    // This is your single object
    if case .single(let single) = response.color {
        // Every single object may be an Int or a String
        if case .int(let int) = single {
            print(int)
        }
        if case .string(let string) = single {
            print(string)
        }
    }

    // This is your array
    if case .array(let array) = response.color {
        array.forEach({ (element) in
            // Each element of your array may be an Int or a String
            if case .int(let int) = element {
                print(int)
            }
            if case .string(let string) = element {
                print(string)
            }
        })
    }
} catch {
    print(error)
}

它可以处理的数据类型是:

您可以尝试将上面 JSON 中键 color 的值替换为这些对象之一

  1. String: "FF2233"
  2. Int: 223344
  3. [String]: ["FF2233", "FF2234", "FF2235"]
  4. [Int]: [223344, 223345, 223346]
  5. String/Int 混合数组["FF2233", "FF2234", 223344, 223345, "FF2235", 223346]

The most surprising fact about this design is that you can parse Mixture of [Int & String]