在Swift中合并可编码

时间:2018-10-25 07:55:20

标签: swift codable

我有以下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)

我无法更改json格式,因为它是固定网络API。我可以将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
    }
}

3 个答案:

答案 0 :(得分:1)

使用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"
}

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

答案 1 :(得分:1)

调用每个可编码对象的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)
    }
}

答案 2 :(得分:0)

我想在这里扩展@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:)。同样,如果一个人使用 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) } } ,那么其他人也必须如此。
  3. 如果您使用键控容器,那么所有组合对象中的两个变量都不能共享相同的键名!否则,您会发现它们会相互覆盖,因为它们被解析到同一个字典中。

可以缓解这些问题但不会产生相同 JSON 结构的替代方法是:

container.encode()

现在,这有点不太方便,因为我们不能简单地提供符合 Encodable 的对象数组;它需要确切地知道他们将如何调用 struct EncodableCombiner: Encodable { let elementA: MyEncodableA let elementB: MyEncodableB } 。结果是一个以数组为根对象的 JSON 对象,每个子元素都表示为该数组中的一个元素。事实上,你可以像这样进一步简化它:

MyEncodableA

... 这当然会导致字典根对象的编码形式为 elementA,键为 MyEncodableBelementBSymfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException The GET method is not supported for this route. Supported methods: POST.

这完全取决于您想要什么结构。