0

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...

rgajrawala
  • 2,148
  • 1
  • 22
  • 35

1 Answers1

1

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

Shadowrun
  • 3,572
  • 1
  • 15
  • 13
  • Unfortunately, I got some pushback for including a new dependency just for this use case. Took a look at the code though and it seems they just manually check all the types that conform to Codable. I ended up writing something similar for the struct: https://gist.github.com/rgajrawala/f3d009b2505f67f8ca3c28f317f03f0b. If there was no pushback I'd just use AnyCodable, so I'm marking your answer. Thanks! – rgajrawala Mar 18 '21 at 19:28
  • Turns out you can also use associated value enums which are more elegant than having a struct with a member for each type. I have updated the gist to show that. You can also use computed property in enum to pin key and the value type so that way developers can't use the wrong type on accident. Enums are a lot more powerful than I first thought, will have to take a second look at them! – rgajrawala Mar 19 '21 at 18:19