在Swift中合并Encodable

10
我有以下的Swift结构体。
struct Session: Encodable {
    let sessionId: String
}
struct Person: Encodable {
    let name: String
    let age: Int
}

let person = Person(name: "Jan", age: 36)
let session = Session(sessionId: "xyz")

我需要将其编码为一个具有以下格式的JSON对象:
{
  "name": "Jan",
  "age": 36,
  "sessionId": "xyz"
}

Session的所有键合并到Person的键中

我考虑使用一个带有自定义Encodable实现的容器结构,其中使用SingleValueEncodingContainer,但它显然只能编码一个值

struct RequestModel: Encodable {
    let session: Session
    let person: Person

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(person)
        // crash
        try container.encode(session)
    }
}

let person = Person(name: "Jan", age: 36)
let session = Session(sessionId: "xyz")
let requestModel =  RequestModel(session: session, person: person)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try encoder.encode(requestModel)
let json = String(data: data, encoding: .utf8)!

print(json)

由于网络API是固定的,我无法更改json格式。虽然我可以将sessionId作为Person的属性,但我希望避免这样做,因为它们是不相关的模型。

另一种方式是让RequestModelSessionPerson复制所有属性,如下所示,但这并不好,因为我的实际结构有更多的属性。

struct RequestModel: Encodable {
    let sessionId: String
    let name: String
    let age: Int

    init(session: Session, person: Person) {
        sessionId = session.sessionId
        name = person.name
        age = person.age
    }
}
4个回答

14

调用每个可编码对象的encode(to:)方法,而不是singleValueContainer()方法。这样可以将多个可编码对象合并成一个可编码对象,而无需定义额外的CodingKeys

struct RequestModel: Encodable {
    let session: Session
    let person: Person

    public func encode(to encoder: Encoder) throws {
        try session.encode(to: encoder)
        try person.encode(to: encoder)
    }
}

非常感谢!如此简单却又如此优雅。 - manmal
1
它只对encode函数中的最后一个项目即person进行编码。有任何想法为什么? - Hudi Ilfeld
@HudiIlfeld 当然可以。在你的 encode(to:) 实现中,你指定要使用单值容器。因此,输出只能包含一个值,所以后续编码会覆盖它。如果你需要将多个对象组合成一个 JSON 输出,请确保它们都使用相同类型的容器,并且该容器必须是键控或非键控值容器,具体取决于你想让你的 JSON 是数组还是字典。不要混用,也不要使用单值容器。 - Ash

4
使用encoder.container(keyedBy: CodingKeys.self)代替singleValueContainer(),并分别添加键值对,即:
struct RequestModel: Encodable
{
    let session: Session
    let person: Person

    enum CodingKeys: String, CodingKey {
        case sessionId, name, age
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(person.age, forKey: RequestModel.CodingKeys.age)
        try container.encode(person.name, forKey: RequestModel.CodingKeys.name)
        try container.encode(session.sessionId, forKey: RequestModel.CodingKeys.sessionId)
    }
}

输出:

{
  "age" : 36,
  "name" : "Jan",
  "sessionId" : "xyz"
}

如果您仍然遇到任何问题,请告诉我。

4

我想在 @marty-suzuki 的答案上进行拓展,因为有一些微妙之处如果你不小心可能会错过。这是我的代码版本:

struct EncodableCombiner: Encodable {
    let subelements: [Encodable]
    func encode(to encoder: Encoder) throws {
        for element in subelements {
            try element.encode(to: encoder)
        }
    }
}

只需使用可编码对象的数组进行实例化,并将生成的对象视为一个可编码对象。使用此方法时,请记住以下几点:

  1. 您的JSON中只能有一种根对象类型,它可以是单个值、数组或字典。因此,在各种可编码对象中实现encode(to:)时,永远不要使用encoder.singleValueContainer来创建容器。
  2. 您希望组合的每个对象必须使用相同类型的容器,因此,如果其中一个对象使用了unkeyedContainer(),则所有对象都必须使用。同样,如果一个对象使用了container(keyedBy:),其他对象也必须这样做。
  3. 如果您正在使用键控容器,则跨所有组合对象的两个变量不能共享相同的键名!否则,由于它们被解析到同一字典中,您会发现它们互相覆盖。

另一种缓解这些问题的替代方法,但不会产生相同的JSON结构,如下所示:

struct EncodableCombiner: Encodable {
    let elementA: MyEncodableA
    let elementB: MyEncodableB
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(elementA)
        try container.encode(elementB)
    }
}

现在,这种方式不太方便,因为我们不能简单地提供符合 Encodable 协议的对象数组;需要明确知道它们是什么以调用 container.encode()。结果是一个 JSON 对象,其根对象为一个数组,并且每个子元素都表示为该数组中的一个元素。实际上,你可以进一步简化它,如下所示:
struct EncodableCombiner: Encodable {
    let elementA: MyEncodableA
    let elementB: MyEncodableB
}

这将导致一个字典根对象,其中MyEncodableA 的编码形式作为elementA 键,MyEncodableB 则作为elementB

一切取决于您想要的结构。


0
我个人觉得我的版本最方便。
struct BaseParams: Encodable {
    let platform: String = "ios"
    let deviceId: String = "deviceId"
}

struct RequestModel<PayloadType: Encodable>: Encodable {
    let session = BaseParams()
    let payload: PayloadType

    public func encode(to encoder: Encoder) throws {
        try session.encode(to: encoder)
        try payload.encode(to: encoder)
    }
}

使用方法
struct TransactionParams: Encodable {
    let transation: [String]
}

let transactionParams = TransactionParams(transation: ["1", "2"])
let requestModel = RequestModel(payload: transactionParams)

let data = try JSONEncoder().encode(requestModel)
try JSONSerialization.jsonObject(with: data)

结果
["平台": "ios", "交易": ["1", "2"], "设备ID": "deviceId"]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接