JSON serialization with protocols

I am building an app where all major entities of various levels of complexity are persisted in DB as JSON objects. I intend to introduce various implementations for most application-specific components. The framework maintains for the most part objects referenced via protocols but needs to serialize/deserialize objects in a polymorphic manner. It doesn't look like the Coding protocol works well for this dynamic class structure.
I am pretty happy with the deserialization process (global factories that return parsers for each implementation that in turn construct objects). I have a much harder time implementing serialization.
Here is the most basic illustration:

protocol P {
        // ...
 }
class D1: P {
        let x: Int
        // ...
 }
 class D2: P {
        var s: String
        // ...
 }
    
var array: [P]

I want to serialize the array, such that the properties of each object can be restored with little coding within the implementation class. Class identity is preserved in JSON and all properties should be stored in a predictable fashion, for example:

  [
      {
         "type": "D1",
         "args": {
            "x": 3
        },
       "type": "D2",
         "args": {
            "s": "abc"
        }
  ]

The mechanism should be generic enough to accommodate any depth.
I have an idea but it unfortunately requires too much custom coding. Essentially each nonprimitive type should implement JSONRepresentable protocol, and create either a Map or an Array of its members, where each is expected to be a primitive or implementing JSONRepresentable protocol. This is not elegant, is there any standard or recommended way of solving this? Should something better than Codable be implemented?

Have you considered using an enum instead of a protocol?

enum E: Codable {
    case d1(D1)
    case d2(D2)
}

var array: [E] = [.d1(D1()), .d2(D2())]

If you do that – then coding/decoding works out of the box with no custom code required.

As a side note – I'd only use classes if absolutely necessary (in other words use structs by default and only consider classes when there's the actual pressing need).

1 Like

Inheritance is essential here, D types derived from P could and will emerge later and objects derived from P are used in a uniform polymorphic way.
Enumeration is good only when you have a limited well-defined number of cases each of them is treated separately, inheritance keeps uniformity without exposing any implementation details

You could specialize Keyed/SingleValueEncodingContainer and JSONEncoder for your particular protocol P.

protocol P: Codable {}

private struct Wrapped<Base: Codable>: Codable {
  var type = "\(Base.self)"
  let args: Base
}

extension KeyedEncodingContainer {
  mutating func encode(_ p: some P, forKey key: Key) throws {
    try encode(Wrapped(args: p), forKey: key)
  }
}

extension SingleValueEncodingContainer {
  mutating func encode(_ p: some P) throws {
    try encode(Wrapped(args: p))
  }
}

extension JSONEncoder {
  func encode(_ p: some P) throws -> Data {
    try encode(Wrapped(args: p))
  }
}

Then, having for example

struct D1: P { let x: Int, d: D2 }
struct D2: P { let y: String }

let d = D1(x: 3, d: D2(y: "Three"))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
try print(String(data: encoder.encode(d), encoding: .utf8)!)

will result in

{
  "type" : "D1",
  "args" : {
    "x" : 3,
    "d" : {
      "type" : "D2",
      "args" : {
        "y" : "Three"
      }
    }
  }
}
1 Like

This is a step in the right direction.

However, it doesn't work with arrays or dictionaries that include P objects.

My general case is much more complex, there will be many protocols and their derivations that have to de/serailizable to/from JSON, nd future extensions that could automatically comply without changes in framework, as well as various aggregations.

As I said I came up with a solution that is not too bad but requires some customization. It is quite different from Codable protocol which BTW I don't particularly like (although it works great for simple static cases) in part due to a lack of full understanding NS Prtly from my background in different languages.

If anyone is interested I could share my approach