rgajrawala
rgajrawala

Reputation: 2188

Encoding an array of generic structs

I am trying to make an API call that takes a JSON request body like so:

[
  { "op": "replace", "path": "/info/name", "value": "TestName" },
  { "op": "replace", "path": "/info/number", "value": 100 },
  { "op": "replace", "path": "/info/location", "value": ["STATE", "CITY"] },
  { "op": "replace", "path": "/privacy/showLocation", "value": true }
]

I have some enums for the op and path values:

enum ChangeOp: String, Encodable {
  case replace
  case append
}

enum ChangePath: String, Encodable {
  case name = "/info/name"
  case number = "/info/number"
  case location = "/info/location"
  case showLocation = "/privacy/showLocation"
}

In this answer, I found you have to use a protocol to enable creation of array of generic structs, so I have the following protocol and struct:

protocol UserChangeProto {
  var op: ChangeOp { get }
  var path: ChangePath { get }
}

struct UserChange<ValueType: Encodable>: Encodable, UserChangeProto {
  let op: ChangeOp
  let path: ChangePath
  let value: ValueType
}

And here is where the encoding takes place:

func encodeChanges(arr: [UserChangeProto]) -> String? {
  let encoder = JSONEncoder()
  guard let jsonData = try? encoder.encode(arr) else {
    return nil
  }
  return String(data: jsonData, encoding: String.Encoding.utf8)
}

func requestUserChanges(changes: String) {
  print(changes)

  // make API request ...
}

requestUserChanges(changes:
  encodeChanges(arr: [
    UserChange(op: .replace, path: .name, value: "TestName"),
    UserChange(op: .replace, path: .number, value: 100),
    UserChange(op: .replace, path: .location, value: ["STATE", "CITY"]),
    UserChange(op: .replace, path: .showLocation, value: true)
  ]) ?? "null"
)

The issue is that when I try running encoder.encode(arr), I get the following error: Value of protocol type 'UserChangeProto' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols.

My question is, how can I get around this error? Or in other words, what is the simplest way to encode an array of generic structs?

Edit: So it looks like this is an issue with the Swift language itself that the Swift team is looking into. I am not sure how to proceed here...

Upvotes: 0

Views: 401

Answers (1)

Shadowrun
Shadowrun

Reputation: 3857

You might find a type-erased encodable useful: https://github.com/Flight-School/AnyCodable

Using AnyEncodable from the above:

struct Change<V: Encodable>: Encodable {
    enum Op: String, Encodable {
        case replace
        case append
    }
    
    enum Path: String, Encodable {
        case name = "/info/name"
        case number = "/info/number"
        case location = "/info/location"
        case showLocation = "/privacy/showLocation"
    }
    
    var op: Op
    var path: Path
    var value: V
}

let encoder = JSONEncoder()
let changes: [Change<AnyEncodable>] = [
    Change(op: .append, path: .name, value: "Foo"),
    Change(op: .replace, path: .number, value: 42)
]

let r = try? encoder.encode(changes)

String(data: r!, encoding: .utf8)

gives what you would expect

Upvotes: 1

Related Questions