Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merge Encodable in Swift

Tags:

swift

codable

I have the following Swift structs

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")

that I need to encode to a json object that has this format:

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

where all keys of the Session are merged into the keys of the Person

I thought about using a container struct with a custom Encodable implementation where I use a SingleValueEncodingContainer but it can obviously encode only one value

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)

I cannot change the json format as it's a fixed network API. I could have the sessionId as property of the Person but I'd like to avoid that as they are unrelated models.

Another way could be to have the RequestModel copy all the properties from the Session and Person as follows but it's not very nice as my real structs have much more properties.

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
    }
}
like image 208
Jan Avatar asked Oct 25 '18 07:10

Jan


2 Answers

Call encode(to:) of each encodable objects, instead of singleValueContainer(). It makes possible to join multi encodable objects into one encodable object without defining extra 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)
    }
}
like image 114
marty-suzuki Avatar answered Sep 28 '22 08:09

marty-suzuki


Use encoder.container(keyedBy: CodingKeys.self) instead of singleValueContainer() and add the key-value pairs separately, i.e.

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)
    }
}

Output:

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

Let me know in case you still face any issues.

like image 25
PGDev Avatar answered Sep 28 '22 07:09

PGDev