swiftgenericsjsonencoder

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


Solution

  • 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