如何在Swift的Encodable中实现逃逸舱机制
Swift Encodable 逃逸舱实现方案
问题背景
在Swift中使用Encodable协议时,部分JSON API存在结构无法提前确定的字段(如用户自定义的schema场景),需要在严格约束的结构体中保留[String: Any]类型的“逃逸舱”字段,同时保证编码输出与全类型/无类型方案一致。
解决方案
方法一:自定义AnyEncodable类型(推荐,高复用性)
先实现一个能包装任意JSON可序列化类型的AnyEncodable,让[String: Any]可以被Encodable编码器处理:
struct AnyEncodable: Encodable { private let value: Any init(_ value: Any) { self.value = value } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch value { case let number as NSNumber: // 区分NSNumber中的布尔值和数字类型 if number.isBool { try container.encode(number.boolValue) } else { try container.encode(number) } case let string as String: try container.encode(string) case let array as [Any]: try container.encode(array.map(AnyEncodable.init)) case let dict as [String: Any]: try container.encode(dict.mapValues(AnyEncodable.init)) case is Void: try container.encodeNil() default: throw EncodingError.invalidValue( value, EncodingError.Context( codingPath: encoder.codingPath, debugDescription: "Unsupported type: \(type(of: value))" ) ) } } } extension NSNumber { var isBool: Bool { String(cString: objCType) == "c" } }
修改目标结构体的编码逻辑,用AnyEncodable包装逃逸舱字段:
struct RootWithEscapeHatch: Encodable { let a: String let b: [String: Any] private enum Fields: CodingKey { case a case b } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: Fields.self) try container.encode(self.a, forKey: .a) // 用AnyEncodable包装b字段完成编码 try container.encode(AnyEncodable(self.b), forKey: .b) } }
方法二:直接在encode方法中处理(无需额外类型)
如果不需要复用逻辑,可以直接在结构体的encode(to:)方法中通过JSONSerialization转译字段:
struct RootWithEscapeHatch: Encodable { let a: String let b: [String: Any] private enum Fields: CodingKey { case a case b } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: Fields.self) try container.encode(self.a, forKey: .a) // 将[String: Any]转成Data后解码为兼容Codable的类型 let bData = try JSONSerialization.data(withJSONObject: b) let bCodable = try JSONDecoder().decode(AnyCodable.self, from: bData) try container.encode(bCodable, forKey: .b) } } // 简化版AnyCodable实现 struct AnyCodable: Codable { let value: Any init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let int = try? container.decode(Int.self) { value = int } else if let string = try? container.decode(String.self) { value = string } else if let bool = try? container.decode(Bool.self) { value = bool } else if let array = try? container.decode([AnyCodable].self) { value = array.map { $0.value } } else if let dict = try? container.decode([String: AnyCodable].self) { value = dict.mapValues { $0.value } } else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type") } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch value { case let int as Int: try container.encode(int) case let string as String: try container.encode(string) case let bool as Bool: try container.encode(bool) case let array as [Any]: try container.encode(array.map(AnyCodable.init)) case let dict as [String: Any]: try container.encode(dict.mapValues(AnyCodable.init)) default: throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")) } } }
验证结果
运行以下代码:
let myFailingTree = RootWithEscapeHatch(a: "xyz", b: ["c": 2, "d": false]) do { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys let data = try encoder.encode(myFailingTree) print(String(decoding: data, as: UTF8.self)) } catch { print("Could not convert encodable with escape hatch to JSON: \(error)") }
将输出与无类型/严格约束方案一致的JSON:
{"a":"xyz","b":{"c":2,"d":false}}
注意事项
- 两种方案均支持嵌套的
[String: Any]、数组等JSON标准结构 - 若
[String: Any]中包含JSON不支持的类型(如自定义对象),会抛出编码错误,与JSONSerialization行为一致
内容的提问来源于stack exchange,提问作者Lou Zell
相关产品推荐
相关产品推荐

