You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 13:27:35